はじめに
fetch() を書くたびに「あれ、catch に入らないのはなんでだっけ」「catch でエラーを拾えているのにステータスコードが見当たらない」という状況になりがちなので、自分用に整理しておきます。
原因は毎回同じで、fetch() が 2 種類のエラーを全く異なるルートで通知するという仕様を忘れているだけなのですが、何度でも忘れます。
結論:「サーバーと通信できたかどうか」の差
いつも最初にこれだけ思い出せれば十分です。
| 状態 | 意味 |
|---|---|
response.ok === false |
サーバーに到達できた。ただしサーバーがエラーを返した |
catch でエラー |
そもそもサーバーに到達できなかった |
この2つを混同すると、デバッグの方向が真逆になります。
1. response.ok === false になる場合(通信は成功)
fetch() はサーバーとの接続が確立して HTTP レスポンスが返ってきた時点で Promise を resolve します。
ステータスコードが 400 や 500 でも「サーバーが答えた」という事実には変わりないので、reject にはなりません。ここをよく忘れます。
response.ok はステータスコードが 200〜299 のときだけ true になるプロパティで、それ以外は false です。
具体的なケース
| ステータス | 意味 | よくある原因 |
|---|---|---|
400 Bad Request |
リクエストの構文・パラメータが誤っている | バリデーションエラー |
401 Unauthorized |
認証が必要、またはトークンが無効 | Authorization ヘッダー漏れ |
500 Internal Server Error |
サーバー側でエラーが発生 | サーバーのバグ |
503 Service Unavailable |
サーバーがメンテナンス中または過負荷 | デプロイ直後、障害 |
レスポンスボディが存在します
response.ok === false のとき、サーバーはエラーの詳細を JSON やテキストでボディに乗せていることが多いです。await response.json() で読み取れます。
2. catch でエラー(Reject)になる場合(通信が失敗)
サーバーへのリクエストが届かなかったか、受け取れなかったケースです。
fetch() の Promise が reject されて catch ブロックに TypeError が届きます。
HTTP ステータスコードは存在しないので、response.status を見ようとしてもそもそも response オブジェクトがありません。
具体的なケース
| 種別 | 発生条件 |
|---|---|
| オフライン | 端末がインターネットに接続されていない |
| DNS エラー | ドメイン名が存在しない、または名前解決に失敗した |
| CORS エラー | 別オリジンへのリクエストをブラウザが遮断した(サーバーが許可していない) |
| タイムアウト | サーバーの応答が遅すぎて制限時間を超えた |
| SSL/TLS エラー | 証明書が失効・不正・ドメインが一致しない |
| リクエストの中断 |
AbortController.abort() で処理をキャンセルした |
CORS エラーは catch に届く
CORS エラーはネットワークエラーとして扱われるので、response.ok ではなく catch で引っかかります。しかも error.message に詳細が出ないのでわかりにくいです。Console に「CORS policy」と出ていたら Network タブを確認してください。