まず結論
fetch は、404や500が返っても、それだけでは例外にならないことがあります。
通信としてレスポンスを受け取れた場合、fetch 自体は成功扱いになります。そのため、response.ok や response.status を見て判断します。
flowchart TD
A["fetchを実行"] --> B{"Responseを受け取れた?"}
B -->|No| C["network・CORS・abort等を確認"]
B -->|Yes| D{"response.ok?"}
D -->|No| E["statusとAPI契約で分類"]
D -->|Yes| F{"本文を期待形式で読める?"}
F -->|No| G["Content-Type・204・不正JSONを確認"]
F -->|Yes| H["データ構造を検証して利用"]
よくある勘違い
次のコードは、404でも catch に入るとは限りません。
try {
const response = await fetch("/api/posts");
const data = await response.json();
} catch (error) {
console.error("失敗", error);
}
サーバが404を返していても、HTTPレスポンスは返ってきています。ネットワークとしては完了しているため、fetch はresolveされます。
response.status
response.status にはHTTPステータスコードが入ります。
const response = await fetch("/api/posts");
console.log(response.status); // 200, 404, 500 など
statusを見れば、成功なのか、認証エラーなのか、サーバエラーなのかを分けられます。
response.ok
response.ok は、statusが200番台なら true になります。
const response = await fetch("/api/posts");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
これにより、404や500を自分でエラーとして扱えます。
JSON解析エラー
APIがJSONを返す想定でも、実際にはHTMLが返っていることがあります。
例:
- 404ページのHTML
- ログインページのHTML
- サーバエラーページ
その状態で response.json() を呼ぶと、JSON解析エラーになります。
Unexpected token '<'
< はHTMLの先頭であることが多いです。
安全な書き方
const response = await fetch("/api/posts");
if (!response.ok) {
reportApiFailure({ status: response.status, requestId: response.headers.get("x-request-id") });
throw new Error("投稿を読み込めませんでした");
}
const data = await response.json();
利用者へ返す例外は固定メッセージにし、診断情報はアクセス制御された監視先へ送ります。レスポンス本文には個人情報、内部のSQL、認可理由が含まれる可能性があるため、response.text()を例外、画面表示、通常ログへ入れません。
切り分け
| 状態 | 見ること |
|---|---|
| fetchがcatchに入る | ネットワーク断、CORS、URL不正など |
| response.okがfalse | HTTPステータスが200番台ではない |
| json()で落ちる | JSONではないものが返っている |
| statusは200だが画面が変 | データ構造や画面側処理 |
実践メモ: API通信では「fetchが成功したか」と「HTTPとして成功したか」を分けて考えます。
失敗条件と観測項目
エラー処理が十分かは、正常な200だけでは判定できません。テスト用の応答で204、400、401、404、429、500、JSONではない本文、不正なJSONを再現します。各条件で読み込み表示が終了するか、再試行ボタンが必要な時だけ出るか、秘密情報を含む本文が利用者へ露出しないかを観測します。response.okを確認する前に本文を成功データとして描画する、204に対して無条件にJSON解析する、エラー本文を二度読みする設計は失敗条件です。
ログにはstatus、要求先の識別子、発生時刻、追跡用IDを残しますが、認証トークンやレスポンス全文は残しません。画面の文言と開発者向けログを分けると、利用者には安全な案内を出しながら、実装者は原因を追えます。なお、statusだけで意味を決めつけず、そのAPIの契約も確認します。同じ404でも、一覧が空であることを表す設計と、資源が存在しないことを表す設計では画面の扱いが異なります。
よくあるエラーと本文の扱い
response.okを確認してから無条件にresponse.json()を呼んでも、204 No Contentでは本文がないため解析に失敗します。成功時の契約が204を許すAPIでは、statusを先に分岐し、データなしの成功として扱います。反対に、200でもContent-TypeがJSONでなければ、リバースプロキシのエラーページやログインHTMLを受け取った可能性があります。
エラー本文を調査したい場合も、ブラウザの利用者向けコードで文字列化して投げ直しません。サーバ側で発行した追跡IDを受け取り、権限を持つ担当者だけが監視基盤で対応する記録を参照します。診断記録にはstatus、HTTPメソッド、機密値を除いた経路名、追跡ID、発生時刻を含め、Cookie、Authorization、クエリ中の個人情報、レスポンス本文は除外します。利用者表示と診断情報は、内容だけでなく閲覧できる人と保存期間も分けます。
| 応答 | 画面の扱い | 制限された診断情報 |
|---|---|---|
| 204 | 成功として空の状態へ進む | statusと追跡ID |
| 401 | 固定の再認証案内を出す | 認証方式名と追跡ID |
| 404 | 対象がない案内を出す | 資源種別と追跡ID |
| 429 | 待って再試行できる案内を出す | Retry-Afterと追跡ID |
| 500番台 | 固定の障害案内を出す | status、経路名、追跡ID |
動作確認:成功・形式・表示を分ける
テストではfetchがrejectされる通信断、404のJSON、500番台のHTML、204、200の不正JSONを別々に返します。画面に固定文言だけが出ること、読み込み中表示が必ず終了すること、診断関数へ許可した項目だけが渡ることを検証します。response.text()、本文断片、Authorizationが例外メッセージやスナップショットへ現れないことも否定条件として確認します。
成功データについては、JSONとして読めるだけでなく期待する構造かを検証します。ok === trueは業務データの完全性まで保証しません。通信の成立、HTTPの成功、本文形式、データ構造、利用者表示を別の境界として確かめます。
まとめ
判断は、fetchがResponseを返したか、response.okがtrueか、本文を期待形式で解析できたかの順に分けます。404や500はResponseを受け取れるため、通常はokとstatusで分類します。JSON解析はHTTP成功とは別の失敗です。