※本記事にはPRを含む場合があります。

こんにちは。『脱会社員』脱サラを応援し続けるブログ、運営者の『なかびー』です。

JavaScriptのfetchでJSONを取得するときは、応答を待つ、HTTPの状態を確認する、本文をJSONとして読む、必要な項目があるか確かめる、という段階を分けます。fetchが応答を返しただけでは、データ取得が成功したとは限りません。404などのHTTPエラーや、JSONではない応答も別に扱う必要があります。

この記事では二件の架空メモを使い、loading、成功、空の結果、エラー、中止、タイムアウトを整理します。さらに、古いリクエストの結果が新しい結果を上書きしない仕組みも練習します。実際のサービスへ情報を送信する処理、認証、注文や投稿の確定は含みません。

仕様の確認日は2026年10月4日です。MDNとNode.js公式資料を参照し、掲載の完成コードをNode.js 24.19.0で実行しました。組み込みfetchで実行したのはdata URLのJSON読取と不正JSONの確認です。HTTPエラー、通信失敗、中止、時間切れ、応答順逆転は、Responseと差し替え用関数を使う模擬テストで検証しています。外部HTTP通信、ブラウザーのCORS、画面描画は未検証です。画像は説明用の生成イメージです。

目次
  1. fetchの流れを、応答・JSON・データの形に分ける
  2. 小さなJSONを読み、awaitと状態の変化を確かめる
  3. 通信・HTTP・形式・データの失敗を、別々に試す
  4. loading・中止・タイムアウトと、古い応答の上書きを管理する
  5. 秘密情報と再試行の範囲を確認し、検証結果を引き継ぐ

fetchの流れを、応答・JSON・データの形に分ける

応答と内容の確認を分けて考える説明用生成画像

fetchの戻り値は、すぐ使えるJSONではない

fetchを呼ぶとPromiseが返ります。非同期の結果を待ち、得られたResponseから状態や本文を扱う流れです。JSONが必要なら、Responseのjson()などを使って本文を読み取ります。fetchを呼んだ直後の値を、そのままメモの配列として使おうとしないようにします。

async関数の中でawaitを使うと、処理の順序を文章に近い形で書けます。ただし、awaitはプログラム全体を同期処理へ変える命令ではありません。関数の続きが結果を待つ間も、ほかの非同期処理が進む場合があります。このため、複数の取得を始めると完了順が開始順と異なることがあります。

基本の動作は、MDNのFetch APIの使い方で確認できます。最初は複数のAPIを同時に呼ぶより、一つの小さなJSONを取得し、どの段階で何を得たかを確認する方が理解しやすくなります。

HTTPの成功と、データとしての成功を分ける

response.okは、HTTPステータスが200〜299の範囲かを表す値です。たとえば404の応答が返っても、fetch自体が必ず例外になるわけではありません。Responseが得られた後、okやstatusを見て、目的の応答かを判定します。

HTTP上の成功でも、本文が期待するJSONとは限りません。ログイン画面のHTMLが返る、空の本文が返る、JSONの形が変更されているなどの可能性があります。通信の成功、HTTPの状態、本文の形式、項目の形を順に確認すると、原因を区別しやすくなります。

※表は横にスクロールできます。

段階確認すること失敗の例
取得の開始と応答Promiseが正常に完了するか通信失敗や中止
HTTP状態response.okとstatus404・500など
本文の形式想定したContent-TypeかHTMLのログイン画面
JSONの読取json()で本文を解釈できるか壊れたJSONや空の本文
データの形items配列と必要な文字列があるか項目名の変更、型違い
利用する結果空か、必要なデータがあるか取得成功だが対象0件

この段階分けは、エラー表示を増やすためだけではありません。どこまでは成功したかを説明できるようにするためです。「データが出ない」という症状でも、404と空配列では次に取る行動が違います。画面へ何を伝えるかも、状態ごとに考えます。

JSONを読めても、必要な項目があるとは限らない

今回のJSONは、itemsという配列に、idとtitleを持つ二つのオブジェクトが入る形です。idはメモを区別する文字列、titleは表示する文です。配列の中にnullがある、idが数値になっている、同じIDが二つある、といった状態は、この練習の規則ではエラーにします。

JSONはデータの表記形式であり、アプリの業務ルールそのものではありません。構文として正しいJSONでも、必要な項目がなければ使えません。たとえばitemsではなくresultsという項目になっていれば、取得したものを無条件に配列として扱わず、仕様の変更を確認します。

また、JSONを取得できることは、その内容が安全で正しいと証明されることではありません。外部から来る文章を画面へ出す際は、HTMLとして無条件に挿入せず、用途に合う安全な表示方法を選びます。今回は画面描画を省き、取得と状態管理に絞って練習します。

小さなJSONを読み、awaitと状態の変化を確かめる

小さなデータから非同期の順序を学ぶ女性の説明用生成画像

練習は外部送信のないdata URLから始める

以下の完成例では、架空のJSONを文字列にしてdata URLへ入れ、Node.jsの組み込みfetchで読みます。data URLはデータをURL内に持つ形式で、今回のコードは外部のサーバーへリクエストしません。ネットワークや認証の準備なしで、ResponseからJSONを読む処理を確かめるための題材です。

これは本物のHTTP通信を再現する方法ではありません。サーバーの遅延、ネットワークの切断、ブラウザーのCORS、資格情報の送信などは別に検証する必要があります。まず処理の順序を学び、その後に自分が利用を許された同一オリジンのJSONや公式APIへ進む、という段階を分けます。

コードをpractice.mjsという名前で、専用の練習フォルダーへ保存します。Node.jsが使える環境でnode practice.mjsを実行してください。.mjsは、この例をJavaScriptモジュールとして扱い、末尾のawaitを使うための形式です。仕事の既存ファイルへ上書きしないようにします。

async function getNotes(url, { signal, fetcher = fetch } = {}) {
  const response = await fetcher(url, {
    method: 'GET',
    headers: { Accept: 'application/json' },
    signal
  });
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  const type = (response.headers.get('content-type') || '')
    .split(';')[0].trim().toLowerCase();
  if (type !== 'application/json') {
    throw new Error('この例で想定するJSON応答ではありません');
  }
  const data = await response.json();
  if (!data || !Array.isArray(data.items) || data.items.some(item =>
    !item || typeof item.id !== 'string' || item.id.trim() === '' ||
    typeof item.title !== 'string' || item.title.trim() === '')) {
    throw new Error('JSONの項目の形が違います');
  }
  const ids = data.items.map(item => item.id);
  if (new Set(ids).size !== ids.length) {
    throw new Error('IDが重複しています');
  }
  return data.items.map(item => ({ id: item.id, title: item.title }));
}

function createLoader({ onState = () => {}, fetcher = fetch, timeoutMs = 5000 } = {}) {
  let sequence = 0;
  let current = null;
  return {
    async load(url) {
      const requestId = ++sequence;
      current?.abort();
      const controller = new AbortController();
      current = controller;
      let timedOut = false;
      const timer = setTimeout(() => {
        timedOut = true;
        controller.abort();
      }, timeoutMs);
      onState({ status: 'loading', items: [], message: '' });
      try {
        const items = await getNotes(url, { signal: controller.signal, fetcher });
        if (requestId !== sequence) return { status: 'stale' };
        const state = { status: items.length ? 'success' : 'empty', items, message: '' };
        onState(state);
        return state;
      } catch (error) {
        if (requestId !== sequence) return { status: 'stale' };
        const status = timedOut ? 'timeout' : error?.name === 'AbortError' ? 'cancelled' : 'error';
        const message = status === 'timeout' ? '時間内に取得できませんでした' :
          status === 'cancelled' ? '取得を中止しました' : 'データを取得できませんでした';
        const state = { status, items: [], message };
        onState(state);
        return state;
      } finally {
        clearTimeout(timer);
        if (requestId === sequence) current = null;
      }
    },
    cancel() { current?.abort(); }
  };
}

const fixture = { items: [
  { id: 'N1', title: '下書きを確認する' },
  { id: 'N2', title: '参考資料を整理する' }
] };
const localDataUrl = 'data:application/json,' + encodeURIComponent(JSON.stringify(fixture));
const loader = createLoader({ onState: state => console.log(state.status) });
const result = await loader.load(localDataUrl);
console.log(result.items.map(item => item.id).join(','));

Node.js 24.19.0で実行した出力は、順にloading、success、N1,N2でした。最初に読み込み中を通知し、JSONから二件のメモを得て、IDを表示しています。Node.jsのfetchについては公式のグローバルオブジェクト資料で確認できます。ブラウザーとNode.jsでは、利用できる周辺環境やセキュリティの仕組みが同一ではない点も意識してください。

getNotesは、取得した内容を使える形へ整える

getNotesの最初では、GET、Acceptヘッダー、signalをfetcherへ渡しています。fetcherの既定値は組み込みfetchですが、テストでは別の関数へ差し替えられる構造です。普段の取得処理と、意図的な失敗を作るテストを分けられるようにしています。

Responseを得たらokを確認し、この例ではContent-Typeがapplication/jsonであることを確認します。セミコロン以降のcharsetなどは分けて読んでいます。これは今回の練習APIの期待値です。application/problem+jsonなど別のJSON系メディア型をすべて扱う汎用の判定にはしていないため、実際のAPI仕様に応じて見直します。

await response.json()で本文を読み、その後にitemsと各行のid・titleを確認します。余分な項目があっても、返すオブジェクトには必要な二つの文字列だけを取り出します。受け取ったオブジェクトをそのままアプリ全体へ広げるより、ここで必要な形を決めると、後の処理を理解しやすくなります。

response.json()も、待つ処理として扱う

fetchの完了と、本文を最後まで読んでJSONにする処理は別です。response.json()もPromiseを返すため、awaitを付けて結果を待ちます。本文の読取やJSONの構文に問題があれば、この段階でも失敗することがあります。

Responseの本文は、通常一度読み取ると同じ方法で何度も読み直せるものではありません。デバッグのためにtext()で読んだ後、同じResponseへjson()を呼ぶと失敗の原因になります。確認したい形式を決めて一度読み、必要なら事前にcloneするなど、用途に合う方法を考えます。

詳しくはMDNのResponse.json()を参照してください。今回のコードは一度だけjson()を呼び、読めたデータを変数へ保存して使います。JSONの形を調べたい場合も、Responseを何度も消費するのではなく、その変数を確認します。

相対URLへ置き換えるときは、基準を確認する

ブラウザー上のページでfetch(‘/notes.json’)のような相対URLを使う場合は、そのページのオリジンやURLとの関係を確認します。ローカルのファイルをダブルクリックしたfile URLと、HTTPで提供されるページは同じ条件ではありません。ファイルの存在だけで、どの環境からもfetchできるとは考えないでください。

Node.jsで実行する今回の例では、data URLを明示しています。ブラウザー用の相対URLをそのままNode.jsへ貼っても、同じ基準で解決されるとは限りません。どの環境でコードを実行し、どのURLへアクセスするのかを、セットで記録することが大切です。

通信・HTTP・形式・データの失敗を、別々に試す

通信や形式の失敗を切り分ける二人の説明用生成画像

404や500は、response.okを確認して扱う

404は対象が見つからない応答、500はサーバー側のエラーを示す代表的な例です。fetchのPromiseがResponseを返した場合でも、okがfalseなら、今回のgetNotesはErrorを投げます。この分岐がないと、エラー用の本文を正常なデータとして扱おうとすることがあります。

response.okの範囲は、MDNのokプロパティで確認できます。ただし、okがtrueなら必ずJSONがあるわけではありません。たとえば本文がない成功応答を扱うAPIでは、json()を呼ぶ前にその契約に合わせた分岐が必要です。今回の例はJSON本文を返す取得先を想定しています。

エラーページを何度も取得することで解決しようとせず、URLの誤り、利用条件、APIの状態を調べます。404なのに通信が不安定だと考えて無限に再試行すると、原因を見失います。ステータスと段階を記録し、必要な修正へつなげてください。

テスト用のResponseで、失敗を安全に作る

自分が管理していないサーバーへ大量のエラー要求を送る必要はありません。テストでは、fetcherを差し替え、404などのResponseを返す関数を用意できます。次の断片は、上のgetNotesとcreateLoaderがある同じモジュールの末尾で試す想定です。

const mock404 = async () => new Response('{}', {
  status: 404,
  headers: { 'content-type': 'application/json' }
});
const errorLoader = createLoader({
  fetcher: mock404,
  onState: state => console.log(state.status)
});
await errorLoader.load('mock-only');
// loading の後に error

mock-onlyは実在の通信先ではありません。差し替えた関数がURLを使わず応答を作るので、外部のサイトへアクセスせずHTTP状態の処理を試せます。これはサーバーから実際に404を受け取る通信テストとは異なり、404のResponseを受けた後のコードを確認する模擬テストです。

模擬テストの利点は、同じ失敗を何度でも再現しやすいことです。正常なAPIがたまたま停止するのを待つ必要がありません。一方、ネットワーク、TLS、CORS、認証、サーバー側の挙動まで検証したことにはならないため、実際の接続試験とは記録を分けます。

JSONの構文エラーと、項目の形の違いを分ける

JSONが途中で切れている、引用符が欠けているなどの場合は、json()の解析が失敗します。一方、{“items”:”文字列”}のようなJSONは構文として読めても、この例が期待するitems配列ではありません。二つは失敗する段階が違います。

同じIDが複数ある場合も、今回のコードではエラーにします。取得後に一覧の更新や選択を行う設計では、識別子の重複が別の不具合につながることがあるためです。実際のAPIでIDの一意性がどう定義されているかは、その仕様を確認してください。

※表は横にスクロールできます。

失敗条件見つける段階今回の扱い
取得関数が拒否されるfetcherを待つ段階error状態へ移る
HTTP404・500response.okの確認正常データとして読まない
text/htmlの応答Content-Typeの確認想定するJSONと違うと判断
壊れたJSONresponse.json()解析失敗として扱う
itemsが配列でないデータ形状の確認利用する前に止める
IDの重複識別子の確認同じメモを二件として扱わない
itemsが空配列読取と形状確認の後empty状態として成功と区別

利用者へは簡潔な案内を出し、開発者向けには段階を区別できる情報を残す設計が考えられます。今回の状態管理は、画面に出すことを想定したmessageを一般的な文にしています。実際のAPIの応答本文や秘密を、エラー表示へそのまま出す構成にはしません。

CORSエラーは、no-corsを付けるだけで解決しない

ブラウザーでは、別オリジンのデータへアクセスする際にCORSの条件が関係します。スキーム、ホスト、ポートの違いを含むため、見た目が似たURLでも同じオリジンとは限りません。サーバー側が許可する応答ヘッダーなどを確認する必要があります。

mode: ‘no-cors’を付ければ、自由にJSONを読めるようになるわけではありません。不透明な応答では、JavaScriptから必要な本文や状態を読めないことがあります。ブラウザーのセキュリティを無効にしたり、知らない中継サービスへURLや資格情報を渡したりする方法は避けてください。

CORSの仕組みは、MDNのCORS資料で確認できます。Node.jsで取得できたからブラウザーでも同じように読めるとは限りません。本稿のNode.jsテストは、ブラウザーのCORS確認を代替していません。

loading・中止・タイムアウトと、古い応答の上書きを管理する

新しい依頼と古い結果を分けて管理する女性の説明用生成画像

結果が来るまでの状態を、先に決める

非同期の取得では、結果がまだない時間があります。その間を空の一覧だけで表示すると、利用者は本当に0件なのか、まだ読み込んでいるのか分かりません。loadingとemptyを分けることで、同じ空配列でも意味を伝えられます。

今回のcreateLoaderは、開始時にloading、取得後にsuccessまたはempty、失敗時にerror・cancelled・timeoutのいずれかを通知します。onStateは、その状態を受け取るための関数です。サンプルはconsole.logで状態名を出すだけですが、画面へつなぐ場合も、取得の処理と表示の処理を分けて考えられます。

※表は横にスクロールできます。

状態意味利用者へ示す内容の例
loading取得中読み込み中と分かる案内
success一件以上の結果がある取得した一覧
empty正常に読めたが0件対象がないという案内
error取得や内容確認に失敗再確認や再試行の案内
cancelled明示的に中止した中止したことを示す
timeout決めた時間内に終わらなかった時間内に取得できなかった案内
staleより新しい要求がある古い結果画面へ適用せず終了する内部の結果

画面に以前の結果を残したまま読み込み中とする設計もありますが、その場合は古い情報と新しい取得中の状態を区別する必要があります。今回の例は状態ごとにitemsを空にして単純化しています。実際のアプリでは、読み込み中に既存の内容を残すかどうかも利用目的に合わせて決めます。

AbortControllerで、不要になった取得を中止する

AbortControllerのsignalをfetchへ渡し、controller.abort()を呼ぶと、対応する取得や本文の読取を中止するための通知ができます。基本はMDNのAbortControllerとAbortSignalで確認できます。

一度中止したsignalを、次の新しい要求へ使い回さないようにします。今回のコードはloadごとに新しいControllerを作り、前の取得が残っていれば中止します。cancel()は、現在の取得だけへ中止を要求するための入口です。

中止は、サーバー側で実行された処理をすべて巻き戻す操作ではありません。特に送信、購入、投稿などの処理へこの例を広げる場合、中止したから確定していないと判断しないでください。今回は読み取り用のGETを対象にし、状態を変える通信は扱っていません。

タイムアウトの後始末はfinallyで行う

この例は既定で5000ミリ秒を待ち時間の目安にし、時間を過ぎたらControllerへ中止を要求します。5000という値は練習上の設定で、すべてのAPIに適した値ではありません。データ量、利用者の回線、サービス側の条件を考えて決めます。

正常に取得できた場合も、失敗した場合も、finallyでタイマーを止めます。タイマーを残すと、すでに終わった要求へ後から中止を要求するなど、状態の把握が難しくなります。開始した補助処理を、どの終了経路でも片付けることが重要です。

テストでは待ち時間を短くし、signalを受け取って中止する模擬の取得関数を使いました。これは実際の回線が5秒遅延したことを確認した試験ではありません。タイムアウトしたときに状態がtimeoutとなり、利用者向けの案内が返ることを確認しています。

新しい検索より、古い検索が後から返る場合を考える

Aの取得を始めた後にBの取得を始め、Bが先に終わることがあります。その後Aの結果をそのまま表示すると、利用者が最後に選んだBの画面を、古いAが上書きしてしまいます。開始順と完了順が違う可能性を前提に設計しましょう。

今回のコードはsequenceという番号を増やし、各loadが開始時の番号を持ちます。結果が返ったとき、現在の番号と違えばstaleとして返し、onStateへ適用しません。前の取得へabortを要求することに加えて、古い結果を採用しない確認も行う構成です。

このテストでは、意図的に中止通知を無視して遅く結果を返す模擬関数を使い、遅いAと速いBを逆順で完了させました。最後に適用された結果がBで、Aがstaleになることを確認しています。実際の通信条件を完全に再現するものではありませんが、応答の順序が逆転したときの処理を検証できます。

秘密情報と再試行の範囲を確認し、検証結果を引き継ぐ

取得コードの検証範囲を記録して引き継ぐ男性の説明用生成画像

ブラウザーへ配るコードに、秘密鍵を埋め込まない

ブラウザーで動くJavaScriptは、利用者へ配られるコードです。そこへ秘密のAPIキーや管理者のトークンを直接書き込むと、秘密として守れません。フロントエンドの環境変数という名前を付けても、ビルド後のコードへ含まれるなら同じ問題があります。認証が必要なAPIは、そのサービスが想定する安全な構成を確認します。

URLのクエリへ秘密や個人情報を入れることにも注意します。ログ、履歴、共有されたリンクなどに残る可能性があります。今回のdata URLは架空の二つのメモだけを含み、実在の利用者や認証情報を入れていません。練習の値を本物の機密データへ置き換えないでください。

資格情報を送る設定を変える場合も、何がどの相手へ送られるかを確認します。ブラウザーのCookieなどの扱いは、オリジンやcredentialsなどに関係します。「GETだから何も情報が送られない」とは考えず、リクエスト先と付随する情報を把握したうえで使います。

再試行は、回数と対象を決めて行う

一時的な失敗で再試行が役立つ場合はありますが、すべてのエラーへ無限に繰り返す方法は避けます。404やデータ形状の変更は、同じ要求を繰り返すだけでは直らないことがあります。制限や待ち時間の案内があるAPIでは、その利用規約と応答の指示に従います。

今回の完成例は自動再試行を実装していません。必要であれば、読み取り処理に限定して、回数、間隔、中止条件、利用者の操作との関係を設計します。購入や投稿など状態を変える処理を、応答が見えないからという理由で繰り返すと、重複した処理が起きる可能性があります。

再試行する前に、前回の要求が終わっているか、中止を要求しただけなのか、新しい要求と競合していないかを確認します。今回のsequenceのような考え方は結果の上書き防止に役立ちますが、サーバー側の重複処理を防ぐ機能とは別です。役割を広げて解釈しないようにしましょう。

実行した十二条件と、未検証の通信を分ける

本稿のテストでは、data URLの正常読取、空の結果、404、500、Content-Type違い、不正JSON、項目の形違い、ID重複、取得関数の拒否、時間切れ、明示中止、古い応答の抑止という十二条件を確認しました。HTTP応答の分岐などは、制御したResponseと差し替え関数による模擬テストです。

※表は横にスクロールできます。

検証した範囲方法結果の読み方
正常なJSON読取Node.js組み込みfetchとdata URLloading→success、N1/N2を取得
不正なJSONdata URLの不正本文JSON解析が失敗する
404・500・形式・項目Responseを返す模擬関数各分岐が正常扱いしない
通信失敗の扱い模擬関数がTypeErrorで拒否error状態へ移る
中止と時間切れsignalへ応じる模擬関数cancelled/timeoutを区別
古い結果の抑止遅い結果と速い結果を逆順に返す新しい結果を維持
外部HTTP・CORS・画面本稿では未実施実接続時に別途確認が必要

実際のAPIへ接続するときは、正規のURL、利用を許されたデータ、認証の方法、CORS、応答形式、制限を確認します。テストが通ったという理由だけで、別のサービスへ同じコードを無条件に向けないでください。今回の検証結果は、処理の分岐を理解するための土台です。

失敗を相談するときは、秘密を除いて段階を伝える

相談用の記録には、実行環境、取得先の種類、発生した段階、HTTP状態が確認できたか、期待するデータの形を残します。認証トークン、Cookie、顧客の本文などを、そのまま公開の質問へ貼り付けないようにしてください。必要な情報だけを架空の値へ置き換えると、安全に再現例を作りやすくなります。

「fetchが動きません」だけより、「Responseは返るがContent-Typeがtext/htmlだった」「JSONは読めるがitemsが配列ではなかった」と書く方が、確認する場所を絞れます。エラーの文字だけを変えて消すのではなく、どの前提が違ったかを調べることが大切です。

小さな取得処理を、説明できる学習成果にする

学習記録としては、入力のJSON、成功時の結果、失敗条件、実行環境、未検証の接続条件を一組で残します。今回なら「取得とJSON読取を分け、状態を通知し、古い応答を適用しない」と説明できます。処理を短く書くことより、成功と失敗のどちらも説明できることが実務へつながります。

作品のまとめ方は、プログラミングのポートフォリオとREADMEの作り方も参考にしてください。学習支援を比べたい場合は、副業向けプログラミングスクールの比較で、費用や支援内容を確認する方法もあります。この例の完成や受講だけで、仕事の獲得や収入が保証されるわけではありません。

JavaScriptのfetchは、取得する一行だけでなく、応答の確認、JSONの検証、待っている間の状態、失敗後の処理までを一つの流れとして学ぶと使いやすくなります。まずは外部送信のない小さな例で分岐を確かめ、実際の接続は利用条件と検証範囲をそろえて進めましょう。