fetchで404がcatchに入らない理由|HTTP・JSON・データ形式の失敗を分ける
fetchは404でも応答を返します。HTTPエラー、本文の読み取り、JSONの構文、必要なデータの欠落をどう分けるか。ローカルで動くサンプルで、204の空本文や二重読み取りも確認します。

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、okはfalseになります。エラー本文自体は正しい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判定が自動で追加されるわけではありません。
確認した一次情報
- MDN: Fetchの利用と応答本文の読み取り確認日: 2026/09/08
- MDN: Response.ok確認日: 2026/09/08
- MDN: Response.jsonの結果と例外確認日: 2026/09/08



