fetchが404でもcatchに入らない原因|response.okを見ないと、エラーなのに成功扱いになる
fetch() は、404や500が返ってきても catch に入らない。
サーバーから返事が届いた時点で成功扱いになるのがMDNに書かれた決まり。
response.ok を見て自分で投げる直し方と、catch に入るのがどんなときかをまとめた。
fetch() で404や500が返ってきても、catch には入らない。
ネットワークが切れたときだけ catch に入り、サーバーが「そのページは無い」と答えたときは、普通に成功した顔で処理が先へ進む。
直し方は、レスポンスが返ってきたら response.ok を見て、false なら自分で例外を投げる、の1行だ。
この記事は、MDNの説明を読んで整理したものだ。
「僕は〜と思う」と書いたところだけが、僕の意見になる。
catch に入らないのは、バグではなく fetch の決まり
MDNの「Fetch の使用」には、はっきり書いてある。
fetch() が返すプロミスは、ネットワークエラーや不正なスキームのようなエラーでは拒否される。
でも、サーバーが404のようなエラーのステータスで応答したときは、拒否されない。Response で履行される、という書き方だ。
つまり fetch にとっては、サーバーから返事が届いた時点で「仕事は成功」になる。
返事の中身が「見つかりません」でも、「サーバーでエラーが起きました」でも、返事は返事だからだ。
だからこう書いても、404のときに catch は動かない。
try { const response = await fetch("/api/items.json"); const data = await response.json(); console.log(data);} catch (error) { console.error("失敗しました");}この書き方だと、「失敗しました」が出るのは、通信そのものが失敗した場合だけになる。
直し方:response.ok を見て、false なら自分で投げる
MDNの例も、同じ形で直している。
fetch() のあと、本文を読む前にステータスを調べて、ダメなら throw で例外にする。
try { const response = await fetch("/api/items.json"); if (!response.ok) { throw new Error(`レスポンスステータス: ${response.status}`); } const data = await response.json(); console.log(data);} catch (error) { console.error(error.message);}MDNの説明では、Response.ok は、ステータスが200〜299の範囲なら true になる論理値だ。
200だけではなく、201や204も true になる。
リダイレクトのあと最終的に200が返ってきた場合も、見るのは最後のレスポンスなので true になる。
これで404も500も、throw したところから catch に入る。
エラーメッセージにステータスの数字を入れておくと、あとでコンソールを見たときに、404なのか500なのかがすぐ分かる。
ok を見ないと、エラーが別の場所に出る
ok を見ない書き方で、404のページに当たったとする。
たとえば、JSONのつもりで取りにいったURLが404で、返ってきたのがHTMLの「ページが見つかりません」の画面だった場合だ。
その場合、次の response.json() がHTMLを読んで失敗する。
JSONとして読めない文字列が来たからで、この失敗は catch に入る。
ここで厄介なのは、画面に出るエラーが「JSONの形が違う」になって、本当の原因の「URLが違う」が見えなくなることだ。
僕は、原因とは別のところにエラーが出るのが、いちばん時間を食う種類の不具合だと思っている。
fetch の直後で ok を見ておけば、エラーは「404だった」という本当の原因のまま、catch に届く。
逆に、catch に入るのはどんなとき
fetch が拒否されるのは、MDNの説明では、ネットワークエラーなどで返事そのものが得られなかったときだ。
オフラインのとき、サーバーに届かなかったとき、スキームが不正なとき、がこれに当たる。
もうひとつ、CORSの失敗も入る。
MDNには、プリフライトリクエストを送ったのにサーバーが必要なヘッダーを返さないと、ブラウザーは呼び出し元にネットワークエラーを返す、とある。
別のドメインのAPIを呼ぶときに、サーバー側の設定が足りないと、これで catch に入る。
整理すると、こうなる。
- 通信できなかった・CORSで止められた:fetch が拒否される。何もしなくても catch に入る。
- 返事は届いたが、404や500だった:fetch は成功扱い。
response.okを見て自分で投げないと、catch に入らない。
毎回書くのが面倒なら、関数にまとめる
fetch を呼ぶ場所が何か所もあるなら、ok のチェックを1つの関数に入れてしまうほうが、書き忘れが起きない。
async function fetchJson(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`${response.status} ${url}`); } return response.json();}呼ぶ側は await fetchJson("/api/items.json") と書くだけで、404も500も例外になって、catch で受けられる。
ステータスごとに処理を変えたいときだけ、この関数の中で response.status を見て分ければいい。
僕のチェックの仕方
fetch を書いたら、まず存在しないURLを1回わざと呼んで、画面とコンソールに何が出るかを見るようにしている。
エラーが出ないで、画面が空のままになるなら、ok の確認が抜けている。
この記事に載せたコードは、MDNの例をもとに僕が書き直したものだ。
そのまま貼る前に、自分のプロジェクトのURLで、404のときの動きを1度見てほしい。
ボタンの二重送信の話は一回しか押していないのに申し込みが二件届く、async まわりの落とし穴はforEachの中でawaitしても待たないに書いた。
出典
- Fetch の使用 - Web API | MDN(レスポンスステータスのチェック・CORS)
- Response: ok プロパティ - Web API | MDN
ほかにも書いています
受託・note・スタンプこういう仕組みを作る相談を受けています
毎日の更新・データ集め・集計の自動化/3万円〜収益がいくらだったか、どの施策が外れたか
note / 個人の視点で掘り下げていますスタンプラリーに挑戦する
行った場所は、現地チェックインで記録に残せます※受託の相談とnoteは、運営者が個人で受けている・書いているものです。
