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

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

正規URL: https://3mikan.com/archives/6086

著者: みかん
公開: 2026-09-08T15:06:00.000Z
更新: 2026-09-08T15:06:00.000Z

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

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

## まずは手元で再現する

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

```sh
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解説](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)

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

`ok`はステータスが200〜299なら`true`になるプロパティです。「画面に必要なデータがそろっている」という保証ではありません。[Response.okの定義](https://developer.mozilla.org/en-US/docs/Web/API/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として正しい値と、アプリが必要とする値を比べてみます。

```js
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の例外](https://developer.mozilla.org/en-US/docs/Web/API/Response/json)

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

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

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

```js
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()`へ渡せば済みます。実際の応答を記録するときは、個人情報や認証情報までログに出さないようにしてください。[応答本文の読み取りに関する説明](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#locked_and_disturbed_streams)

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

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

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

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

## 確認した一次情報

- [MDN: Fetchの利用と応答本文の読み取り](<https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch>): 確認日 2026-09-08
- [MDN: Response.ok](<https://developer.mozilla.org/en-US/docs/Web/API/Response/ok>): 確認日 2026-09-08
- [MDN: Response.jsonの結果と例外](<https://developer.mozilla.org/en-US/docs/Web/API/Response/json>): 確認日 2026-09-08
