WithCodeMedia-1-pc
previous arrowprevious arrow
next arrownext arrow

WithCodeMedia-1-sp
previous arrowprevious arrow
next arrownext arrow

fetch APIの使い方|非同期通信の基本からエラー処理まで

生徒

APIからデータを取ってきて画面に出したいんですが、fetchの書き方がいまいち掴めません…。

ペン博士

fetchは「投げる・待つ・受け取る」の3段階で考えると簡単だよ。async/awaitで書けば見た目もほぼ同期処理と同じになる。落とし穴のエラー処理まで順番に見ていこう。

外部のAPIからデータを取得する、フォームの内容をサーバーへ送る、ページを再読み込みせずに一覧を更新する。こうした処理の入口がfetch APIです。

かつてはXMLHttpRequestという長い記述が必要でしたが、fetchなら数行で書けます。ブラウザに標準搭載されているため、ライブラリの読み込みも不要です。

ただし1つだけ有名な落とし穴があります。404や500が返ってきてもcatchに入らないという挙動です。この記事では基本の書き方から、その対処、実務で必要になるエラー処理・タイムアウト・並列取得までまとめて解説します。

  • fetchの基本構文(3つのステップ)
  • async/awaitで読みやすく書く
  • 404・500がcatchされない問題と対処
  • GETでデータを取得する
  • POSTでデータを送信する
  • JSON以外(テキスト・画像・FormData)の扱い
  • タイムアウトと中断(AbortController)
  • 複数のリクエストを並列で処理する
  • CORSエラーの原因と対処
  • 実務で使える汎用関数

目次

fetchの基本構文

fetchの基本構文

fetchは「リクエストを投げる」「レスポンスを受け取る」「中身を取り出す」の3段階で動きます。

// 最小の書き方
fetch('https://api.example.com/posts')
  .then(response => response.json())   // 中身をJSONとして取り出す
  .then(data => console.log(data))     // 取り出した結果を使う
  .catch(error => console.error(error));
段階何をしている戻り値
fetch(url)リクエストを送るResponseを含むPromise
response.json()本文をJSONに変換データを含むPromise
data実際の値を使うオブジェクトや配列

重要なのは、fetchが返すのがデータそのものではなくResponseオブジェクトだという点です。ステータスコードやヘッダーの情報が入っており、本文は.json()などで別途取り出します。

async/awaitで読みやすく書く

.then()の連鎖は処理が増えると読みにくくなります。実務ではasync/awaitで書くのが主流です。

async function getPosts() {
  try {
    const response = await fetch('https://api.example.com/posts');
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error('取得に失敗しました:', error);
  }
}

getPosts();

awaitは「この行が終わるまで待つ」という意味です。上から下へ順に読めるため、処理の流れがそのままコードの並びになります。

注意点として、awaitasync関数の中でしか使えません。関数の外で使うとエラーになります(モジュール直下のトップレベルawaitは例外)。

404・500がcatchされない問題

404・500がcatchされない問題

fetch最大の落とし穴です。サーバーが404や500を返しても、通信自体は成功しているためcatchに入りません。

// 危険な書き方:404でもエラーにならない
async function bad() {
  try {
    const res = await fetch('https://api.example.com/not-found'); // 404
    const data = await res.json();   // ここでJSON解析エラーになることも
    console.log(data);
  } catch (e) {
    console.error(e);   // 期待した場所で捕まらない
  }
}

catchに入るのは、ネットワーク断・CORS違反・URLが不正といった通信自体が失敗した場合だけです。HTTPステータスは自分で判定します。

// 正しい書き方:res.ok を必ず確認する
async function getPosts() {
  const res = await fetch('https://api.example.com/posts');

  if (!res.ok) {
    // 200〜299以外はここで弾く
    throw new Error(`HTTPエラー: ${res.status} ${res.statusText}`);
  }

  return await res.json();
}
状況catchに入る?判定方法
通信断・オフライン入るcatchで捕まえる
CORS違反入るcatchで捕まえる
404 Not Found入らないres.okで判定
500 サーバーエラー入らないres.okで判定
JSONの形式が不正入る.json()で例外

res.okステータスが200〜299のときtrueになる便利なプロパティです。fetchを書いたら必ずこの判定を入れる、と覚えてください。

GETでデータを取得する

GETでデータを取得する

最もよく使う形です。取得したデータをDOMに反映するところまで通しで書きます。

async function renderPosts() {
  const list = document.querySelector('#post-list');
  list.textContent = '読み込み中…';

  try {
    const res = await fetch('https://api.example.com/posts?limit=10');
    if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);

    const posts = await res.json();

    // 一度に組み立ててから差し込む(描画回数を減らす)
    list.innerHTML = posts
      .map(post => `<li><a href="${post.url}">${post.title}</a></li>`)
      .join('');
  } catch (error) {
    list.textContent = 'データを取得できませんでした。時間をおいてお試しください。';
    console.error(error);
  }
}

document.addEventListener('DOMContentLoaded', renderPosts);

クエリパラメータが多い場合は、文字列を手で連結せずURLSearchParamsを使うと安全です。

const params = new URLSearchParams({
  category: 'css',
  limit: 10,
  keyword: '中央寄せ',    // 日本語も自動でエンコードされる
});

const res = await fetch(`https://api.example.com/posts?${params}`);

POSTでデータを送信する

POSTでデータを送信する

送信では、メソッド・ヘッダー・本文の3つを指定します。

async function sendContact(formData) {
  const res = await fetch('https://api.example.com/contact', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: formData.name,
      email: formData.email,
      message: formData.message,
    }),
  });

  if (!res.ok) throw new Error(`送信に失敗しました: ${res.status}`);
  return await res.json();
}

bodyには文字列を渡す必要があります。オブジェクトをそのまま渡しても送信されないため、JSON.stringify()で変換します。

フォームの値をまとめて送る場合は、FormDataを使うと入力欄を1つずつ拾う必要がなくなります。

const form = document.querySelector('#contact-form');

form.addEventListener('submit', async (event) => {
  event.preventDefault();   // ページ遷移を止める

  const res = await fetch('/api/contact', {
    method: 'POST',
    body: new FormData(form),   // Content-Typeは自動で付く
  });

  if (res.ok) {
    form.reset();
    alert('送信しました');
  }
});

FormDataを使うときはContent-Typeを自分で書きません。境界文字列を含むヘッダーをブラウザが自動生成するため、手で指定すると壊れます。

JSON以外のデータを扱う

レスポンスの取り出し方は、返ってくる形式によって変えます。

メソッド戻り値使う場面
.json()オブジェクト・配列API全般
.text()文字列HTML・CSV・プレーンテキスト
.blob()バイナリ画像・PDFのダウンロード
.formData()FormDataフォーム形式の応答
.arrayBuffer()生バイト列音声・独自形式
// テキストとして取得
const html = await (await fetch('/parts/header.html')).text();

// 画像を取得して表示する
const res = await fetch('/img/photo.jpg');
const blob = await res.blob();
document.querySelector('#preview').src = URL.createObjectURL(blob);

注意点として、レスポンスの本文は一度しか読み取れません.json()を呼んだあとに.text()を呼ぶとエラーになります。両方必要ならres.clone()で複製してください。

タイムアウトと中断

タイムアウトと中断

fetchには標準のタイムアウト設定がありません。応答が返らないと待ち続けるため、自分で打ち切ります。

async function fetchWithTimeout(url, options = {}, timeout = 8000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    const res = await fetch(url, { ...options, signal: controller.signal });
    if (!res.ok) throw new Error(`HTTPエラー: ${res.status}`);
    return await res.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('通信がタイムアウトしました');
    }
    throw error;
  } finally {
    clearTimeout(timer);   // 成功しても必ず解除する
  }
}

AbortControllerは中断にも使えます。検索の入力途中で古いリクエストを打ち切る、といった処理が代表例です。

let currentController = null;

async function search(keyword) {
  currentController?.abort();          // 前の検索を中断
  currentController = new AbortController();

  const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`, {
    signal: currentController.signal,
  });
  return await res.json();
}

複数のリクエストを並列で処理する

複数のAPIを叩く場合、順番にawaitすると待ち時間が積み重なります。同時に投げれば最も遅い1本の時間で済みます。

// 遅い書き方:合計3秒かかる
const a = await fetch('/api/a');   // 1秒
const b = await fetch('/api/b');   // 1秒
const c = await fetch('/api/c');   // 1秒

// 速い書き方:最も遅い1本ぶん(約1秒)
const [resA, resB, resC] = await Promise.all([
  fetch('/api/a'),
  fetch('/api/b'),
  fetch('/api/c'),
]);

Promise.all1つでも失敗すると全体が失敗します。一部が失敗しても続行したい場合はPromise.allSettledを使います。

const results = await Promise.allSettled([
  fetch('/api/a').then(r => r.json()),
  fetch('/api/b').then(r => r.json()),
]);

results.forEach((result, i) => {
  if (result.status === 'fulfilled') {
    console.log(`${i}番目 成功`, result.value);
  } else {
    console.warn(`${i}番目 失敗`, result.reason);
  }
});

CORSエラーの原因と対処

CORSエラーの原因と対処

fetchでよく遭遇するのがCORSエラーです。「別のドメインへのアクセスを、相手サーバーが許可していない」という意味になります。

Access to fetch at 'https://api.example.com/data' from origin
'https://mysite.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • 原因:APIサーバー側が許可ヘッダーを返していない
  • 誤解:JavaScript側の書き方では解決できない
  • 対処1:APIの提供元で許可設定を行う(自社APIの場合)
  • 対処2:自分のサーバーを経由させる(プロキシ)
  • 対処3:APIキーが必要なものは、そもそもブラウザから直接叩かない

よく見かけるmode: 'no-cors'解決策ではありません。エラーは消えますが、レスポンスの中身が読めない状態(opaque)になるだけです。

// これはCORSの解決策ではない
const res = await fetch(url, { mode: 'no-cors' });
console.log(res.status);   // 常に 0。中身も読めない

なお、APIキーやトークンをブラウザのJavaScriptに書くと、閲覧者に丸見えになります。認証が必要なAPIはサーバー側から呼び出してください。

実務で使える汎用関数

動かない時の原因

ここまでの内容をまとめた、そのまま使える関数です。エラー処理・タイムアウト・JSON判定を含みます。

/**
 * fetchのラッパー。HTTPエラー・タイムアウトを例外として扱う。
 * @param {string} url
 * @param {object} options fetchのオプション
 * @param {number} timeout ミリ秒。既定8秒
 */
async function request(url, options = {}, timeout = 8000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    const res = await fetch(url, { ...options, signal: controller.signal });

    if (!res.ok) {
      // サーバーがエラー詳細を返す場合に備えて本文も読む
      let detail = '';
      try {
        detail = await res.text();
      } catch (_) { /* 読めなくても続行 */ }
      throw new Error(`HTTP ${res.status} ${res.statusText} ${detail.slice(0, 120)}`);
    }

    // 本文が無い応答(204など)に備える
    if (res.status === 204) return null;

    const type = res.headers.get('content-type') || '';
    return type.includes('application/json') ? await res.json() : await res.text();
  } catch (error) {
    if (error.name === 'AbortError') throw new Error('通信がタイムアウトしました');
    throw error;
  } finally {
    clearTimeout(timer);
  }
}

// 使い方
const posts = await request('/api/posts');
const created = await request('/api/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: '新しい記事' }),
});

この関数を1つ用意しておけば、各所でres.okの判定を書き忘れる事故がなくなります。

うまく動かないときの確認手順

fetchが期待通りに動かない場合、次の順で確認すると原因にたどり着けます。

症状原因確認・対処
catchに入らないのに失敗HTTPエラーres.okを判定する
[object Object]が送られるbodyが文字列でないJSON.stringifyする
CORSエラーサーバー側の許可なしAPI提供元かプロキシで対応
awaitで構文エラーasync関数の外関数にasyncを付ける
本文が2回読めない既に消費済みres.clone()を使う
ずっと待ち続けるタイムアウト未設定AbortControllerを使う
データがundefinedawaitの付け忘れ.json()にもawaitを付ける

最後の項目は初学者に多いミスです。.json()自体もPromiseを返すため、awaitを2回書くのが正しい形になります。

WordPressで使う場合の注意点

WordPressサイトでfetchを使うときは、REST APIと認証まわりに固有の作法があります。

REST APIから記事を取得する

// 公開記事の取得は認証不要
const res = await fetch('/wp-json/wp/v2/posts?per_page=5&_fields=id,title,link');
const posts = await res.json();

document.querySelector('#latest').innerHTML = posts
  .map(p => `<li><a href="${p.link}">${p.title.rendered}</a></li>`)
  .join('');

_fieldsで必要な項目だけ指定すると、レスポンスが数分の一に軽くなります。記事本文まで含めると1件で数十KBになるため、一覧表示では必ず絞ってください。

更新系はnonceが必要

// 投稿の更新など、書き込み系はnonceを添える
const res = await fetch('/wp-json/wp/v2/posts/123', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': window.wpApiSettings.nonce,   // wp_localize_scriptで渡す
  },
  credentials: 'same-origin',   // ログインCookieを送る
  body: JSON.stringify({ title: '新しいタイトル' }),
});

credentials: 'same-origin'を忘れるとログイン状態が伝わらず、401が返ります。nonceは有効期限が短いため、長時間開いたままの画面では再取得が必要です。

<?php
// functions.php 側でnonceをJSに渡す
add_action( 'wp_enqueue_scripts', function () {
	wp_enqueue_script( 'my-app', get_stylesheet_directory_uri() . '/app.js', [], null, true );
	wp_localize_script( 'my-app', 'wpApiSettings', [
		'root'  => esc_url_raw( rest_url() ),
		'nonce' => wp_create_nonce( 'wp_rest' ),
	] );
} );

この3行を書いておけば、JavaScript側から安全にREST APIを呼べます。APIキーのようにソースへ直書きする必要がありません。

表示速度への配慮

fetchはページ表示後に走るため、書き方によっては読者を待たせます。3点だけ意識してください。

  • 読み込み中の表示を出す:空白のまま待たせない。テキスト1行でも効果がある
  • 失敗時の文言を用意する:何も出ないと壊れたと思われる
  • 高さを先に確保する:後から差し込むと画面が飛び、CLSが悪化する
const list = document.querySelector('#post-list');

// 高さを保った状態で読み込み中を表示する
list.style.minHeight = '240px';
list.innerHTML = '<li class="loading">読み込み中…</li>';

try {
  const posts = await request('/wp-json/wp/v2/posts?per_page=5');
  list.innerHTML = posts.map(p => `<li>${p.title.rendered}</li>`).join('');
} catch (e) {
  list.innerHTML = '<li class="error">読み込みに失敗しました。再読み込みしてください。</li>';
} finally {
  list.style.minHeight = '';
}

min-heightで先に場所を取っておくと、データが届いた瞬間に下の要素が押し下げられる現象を防げます。Core Web VitalsのCLSに直結する配慮です。

対策効果手間
読み込み中の表示離脱を減らす1行
エラー文言不信感を防ぐ1行
min-heightで高さ確保CLSを防ぐ2行
_fieldsで項目を絞る通信量を削減URLに追記

どれも数行で済みますが、体感速度と信頼感に効きます。fetchを書いたらセットで入れる習慣にしてください。


まとめ

fetchは「リクエストを投げる」「Responseを受け取る」「本文を取り出す」の3段階で動きます。async/awaitで書けば上から順に読めるコードになり、実務ではこちらが主流です。最大の注意点は404や500ではcatchに入らないことで、必ずres.okで判定して自分で例外を投げます。POSTではbodyJSON.stringifyで文字列にし、FormDataを使う場合はContent-Typeを自分で指定しないこと。タイムアウトは標準にないためAbortControllerで実装し、複数取得はPromise.allで並列化します。CORSエラーはサーバー側の設定問題で、mode: 'no-cors'では解決しません。

よくある質問(FAQ)

fetchで404が返ってもcatchに入らないのはなぜですか?

fetchは通信が成立した時点で成功とみなす仕様のためです。404や500はサーバーが正常に応答した結果なので、Promiseは解決されます。catchに入るのはネットワーク断・CORS違反・URLが不正な場合だけです。if (!res.ok) throw new Error(...)を必ず書いてください。

awaitはどこに書けばいいですか?

asyncを付けた関数の中でのみ使えます。fetch()と、本文を取り出す.json()の両方がPromiseを返すため、awaitは2回必要です。付け忘れると、データではなくPromiseオブジェクトが変数に入り、undefinedとして扱われる原因になります。

POSTでデータが届きません。

bodyにオブジェクトをそのまま渡していないか確認してください。JSON.stringify()で文字列に変換し、Content-Type: application/jsonを指定する必要があります。逆にFormDataを使う場合は、Content-Typeを手で書くと壊れるため指定しません。

CORSエラーはJavaScript側で解決できますか?

できません。ブラウザがアクセス元を制限する仕組みで、許可を出せるのはAPI提供側のサーバーだけです。自社APIならAccess-Control-Allow-Originを設定し、外部APIなら自分のサーバーを経由させます。mode: 'no-cors'はエラーを消すだけで、応答の中身は読めません。

axiosなどのライブラリを使うべきですか?

fetchで十分なケースがほとんどです。ライブラリはタイムアウトやエラー処理が最初から備わっている利点がありますが、この記事の汎用関数のようなラッパーを1つ作れば同等の使い勝手になります。読み込むファイルを増やさずに済むぶん、表示速度の面でも有利です。

あわせて読みたい関連記事

AIスキルで、未来の自分をアップデート!

今なら完全無料でAIを学べる!WithAI

今なら完全無料でAIを学べる!

  • 動画や実践で楽しく学べる:初心者でも安心のカリキュラム
  • スマホ・PCどちらでもOK:好きな時間に学習できる
  • 料金は一切ナシ0円でAIスキルが身につく

目的に合わせて選べる「AI副業」「AI転職」「AI活用」の3コースを用意。副収入・キャリアチェンジ・日常の生産性アップまで、あなたのゴールに合わせてAIを学べます。

会員登録はカンタン30秒で完了します。まずは公式LINEから、無料でAI学習をスタートしましょう!

この記事を書いた人

WithCodeでWeb制作を習得後、フリーランスエンジニアとして活動。HTML/CSS・JavaScript・WordPress案件を中心に年間20件以上の制作実績を持つ。「難しい技術をわかりやすく」をモットーに、初心者〜中級者向けの技術記事を執筆。副業・フリーランス独立を目指す方に向けた情報発信に注力している。

– service –WithGroupの運営サービス

  • WithCode
    - ウィズコード -

    スクール

    「未経験」から
    現場で通用する
    スキルを身に付けよう!

    詳細はこちら
  • WithFree
    - ウィズフリ -

    実案件サポート

    制作会社のサポート下で
    実務経験を積んでいこう!

    詳細はこちら
  • WithCareer
    - ウィズキャリ -

    就転職サポート

    大手エージェントのサポート下で
    キャリアアップを目指そう!

    詳細はこちら

公式サイト より
今すぐ
無料カウンセリング
予約!

目次