3MIKAN
仮想通貨直コン

fetchで404がcatchに入らない理由|HTTP・JSON・データ形式の失敗を分ける

fetchは404でも応答を返します。HTTPエラー、本文の読み取り、JSONの構文、必要なデータの欠落をどう分けるか。ローカルで動くサンプルで、204の空本文や二重読み取りも確認します。

3MIKANのキャラクターが届いた未開封の小包を手にする、到着しただけでは中身が分からないことを表したイラスト

APIが404を返しているのに、fetch()を囲んだcatchが動かない。逆に、200なのにresponse.json()で止まる。どちらも、応答を受け取れたことと、使えるデータが届いたことは別だと考えると整理できます。

直すときは、response.okでHTTPステータスを確認し、本文を読み取った後で必要な項目を調べます。この記事では、JavaScriptで一覧データを取得する例を使い、どの段階で失敗したかを見分けます。

まずは手元で再現する

実行用サンプルを開く・保存するから、fetch-response-layers.mjsという名前で保存して実行してください。

node fetch-response-layers.mjs

確認環境はNode.js v24.14.0です。追加パッケージやアカウントは不要です。手元の127.0.0.1だけで一時的なHTTPサーバーを起動し、自分自身へアクセスします。外部サービスへの通信やファイルの書き込みはなく、終了時にサーバーを閉じます。

サンプルは結果を表示して期待値と照合します。最後に「すべての実行例が期待値と一致しました。」と出れば、正常例と反例を再現できています。以下のJavaScriptの枠も、それぞれ別の.mjsファイルで実行できます。

404は「応答がなかった」ではない

fetch()は、404や500というステータスだけを理由にPromiseを失敗させません。サーバーから応答を受け取れたので、ステータスや本文を持つResponseが返ります。ネットワークの失敗などとは扱いが異なります。MDNのFetch解説

実行用サンプルの/missingは、404とJSONのエラー本文を返します。await fetch(...)は完了し、statusは404、okfalseになります。エラー本文自体は正しいJSONなので、json()でも読み取れます。

okはステータスが200〜299ならtrueになるプロパティです。「画面に必要なデータがそろっている」という保証ではありません。Response.okの定義

失敗した段階を分けて記録する

今回のサンプルでは、「2xxで、本文をJSONとして読み取れ、itemsが配列である」を成功条件にしています。これはこの一覧取得処理のルールであって、すべてのAPIに共通する条件ではありません。

応答の例 サンプルの判定
接続を応答前に切断 応答取得で失敗
404、JSONのエラー本文 HTTPで失敗
200、HTMLの本文 本文読取・JSONで失敗
204、本文なし 本文読取・JSONで失敗
200、{"items":false} データ形式で失敗
200、{"items":["sample"]} 成功

処理する順番もこの判定に合わせます。最初にfetch()、次にok、その後にjson()、最後にArray.isArray(data?.items)を確認します。HTTPエラーの本文を使わない経路では、サンプルは本文の読み取りをキャンセルします。

APIが204を正常終了として返す設計なら、そこで処理を完了させればよく、無理にjson()を呼ぶ必要はありません。空の本文がJSONとして読めないことを、HTTPの失敗と取り違えないようにします。

jsonが読めても、必要な形とは限らない

JSONとして正しい値と、アプリが必要とする値を比べてみます。

const response = new Response('{"items":false}');
const data = await response.json();
console.log(data.items);                 // false
console.log(Array.isArray(data.items));  // false

ここでは構文エラーは起きません。しかし配列ではないため、そのまま一覧表示には渡せません。実際のAPIでは、配列の各要素に必要なIDや表示名があるかも、APIの仕様に合わせて確認します。今回のサンプルが調べるのはitemsが配列であるところまでです。

json()は、本文を最後まで読んでJSONの値へ変換する処理です。失敗はJSONの構文エラーだけでなく、本文が使用済みだった場合や読み取り・復号の問題でも起こります。そのためサンプルでは、まとめて「JSONが壊れている」と断定せず、「本文読取・JSON」という段階と例外名を記録しています。Response.jsonの例外

また、Content-Typeと実際の本文は別です。サンプルではtext/plainというヘッダーでも、本文がJSONならjson()で読み取れることを確認しています。APIとの約束としてヘッダーを検査するかどうかは、本文の解析とは分けて決めます。

調査用にtextを呼んだ後、同じ本文をjsonで読み直さない

ログを見ようとしてresponse.text()を追加したら、それまで動いていたjson()が失敗することもあります。

const response = new Response('{"items":[]}');
const text = await response.text();
console.log(response.bodyUsed); // true

try {
  await response.json();
} catch (error) {
  console.log(error.name); // TypeError
}

console.log(JSON.parse(text)); // { items: [] }

応答の本文は、一度読み取った同じ流れをもう一度消費できません。この例では、すでに得た文字列をJSON.parse()へ渡せば済みます。実際の応答を記録するときは、個人情報や認証情報までログに出さないようにしてください。応答本文の読み取りに関する説明

修正前に「何を成功とするか」を決める

404がcatchへ入らない問題には、HTTPステータスの確認を足します。200で読み取れない問題には、本文の内容と、すでに読み取っていないかを確認します。JSONは読めるのに画面が壊れるなら、必要な項目や型の確認へ進みます。

このサンプルはNode.js上のローカル通信で、ブラウザーのCORS判定を再現するものではありません。また、失敗を分類できたことと再試行してよいことも別です。まず失敗箇所を取り違えない形にしてから、APIの仕様に沿って表示や再試行を決めましょう。

複数の取得処理をまとめる場合の成否は、Promise.allとallSettledの違いでも確認できます。まとめ方を変えても、各fetch()のHTTP判定が自動で追加されるわけではありません。

確認した一次情報