Fetch は Promise ベースの HTTP クライアント
fetch はブラウザや最近の Node に標準で組み込まれているので、追加で何かをインストールする必要はありません。URL を渡すと Response オブジェクトに解決される Promise が返ってくる、というシンプルな仕組みで、コアの API はこれだけです。
.then が2つ連なっているのは、非同期のステップが2段階あるからです。まずレスポンスヘッダーが届きます(最初の Promise が解決されるのはこのタイミング)。その後にボディを読み取ってパースします(response.json() 自体も Promise を返します)。ボディは、こちらから要求するまでダウンロードされません。
同じ流れを async/await で書くと、上から下に読める普通のコードのように見えます。
await を 2 回使うので、サスペンションポイントも 2 つ。処理内容は同じですが、こちらの方が上から順に読み下しやすくなります。
Response オブジェクトについて
fetch の戻り値はレスポンスボディそのものではなく、Response オブジェクトです。ここにはメタデータに加えて、ボディをさまざまな形式で取り出すためのメソッドが用意されています。
レスポンスボディの読み取りには .json()、.text()、.blob()、.arrayBuffer()、.formData() が使えます。いずれも Promise を返します。ただし、ボディを読めるのは1回だけ。同じレスポンスに対して .json() を2回呼ぶと、2回目は例外になります。
最大の落とし穴: HTTP エラーでは reject されない
fetch に入門したばかりの人がほぼ全員ハマるポイントです。404 や 500 のレスポンスは、実は reject されません。Promise は普通に解決され、response.ok === false になるだけ。fetch が reject するのは、リクエストそのものが完了できなかったとき、つまり DNS 解決の失敗、ネットワーク切断、CORS ブロックなどに限られます。
つまり、何も考えずに fetch を書くと、エラーページの HTML を平然と受け取ってしまい、後から .json() で落ちるハメになります。
response.ok を自分でチェックして、サーバーがエラーステータスを返したら明示的に throw するのが正しい対処法です。
if (!response.ok) のチェックは体に染み込ませておきましょう。自作の fetch ラッパーには、必ず入れておきたいお約束です。
POST リクエストを送る
デフォルトは GET リクエストです。それ以外のメソッドを使いたいときは、第2引数にオプションオブジェクトを渡します。
ここで押さえておきたいポイントが3つあります。
methodのデフォルトは"GET"です。POST・PUT・DELETE・PATCH を使うときは明示的に指定しましょう。bodyに渡せるのは文字列(またはFormData、Blobなど)だけです。fetch はオブジェクトを勝手にシリアライズしてくれないので、JSON.stringify(...)は自分で書く必要があります。Content-Typeヘッダーは、サーバーに対して body のパース方法を伝える役割を持ちます。これを忘れると、多くのサーバーはボディをただのプレーンテキストとして扱ってしまいます。
ヘッダー・クエリ文字列・その他のオプション
ヘッダーはただのオブジェクト(もしくは Headers インスタンス)として渡せます。クエリ文字列は自分で組み立てる形で、普段は URLSearchParams を使うのが定番です。
URLSearchParams を使えば、スペースやアンパサンド、Unicode 文字などのエンコードを自動でやってくれます。エスケープが必要な文字が混ざっていても、URL が壊れる心配はありません。
実際のコードでよく見かける他のオプションも紹介しておきます。クロスオリジンで Cookie を送りたいときは credentials: "include"、HTTP キャッシュを無視したいときは cache: "no-store"、CORS の挙動を制御するには mode: "cors"(通常はこれがデフォルト) といった具合です。
AbortController で fetch リクエストをキャンセルする
途中でリクエストを中断したい場面ってありますよね。ユーザーが検索ワードを打ち直したときや、応答が遅すぎるときなどです。そんなときに使うのが AbortController です。
controller.abort() が呼ばれると、fetch の Promise は DOMException を投げて reject されます。この例外の name は "AbortError" になります。finally ブロックでタイマーをクリアしているのは、リクエストが成功した場合にタイマーが残り続けないようにするためです。
この「fetch + タイムアウト + クリーンアップ」のパターンは、ヘルパー関数にまとめて使い回すのがおすすめです。
再利用できるラッパー関数を作る
ここまでの内容を1つにまとめると、定型コードを一度だけ書けば済む小さなヘルパーができあがります。
ヘッダーの変更箇所はひとつ、エラー処理もひとつ、空レスポンスの扱いもひとつ。ある程度の規模のアプリなら、結局こういう形に落ち着きます。
次は非同期コードのエラーハンドリング
fetch は非同期のエラーが顔を出しやすい代表格で、response.ok のチェックはそのうちのほんの一部でしかありません。次のページでは、Promise と async/await にまたがるエラーハンドリングを取り上げます。エラーがどこへ流れていくのか、どうキャッチするのか、そして気づかないうちにエラーがすり抜けてしまう落とし穴について見ていきましょう。
よくある質問
JavaScriptのfetchはどう使いますか?
基本はfetch(url)にURLを渡すだけ。戻り値はResponseオブジェクトに解決されるPromiseです。レスポンス本文をJSONとしてパースするには、さらにPromiseを返すresponse.json()を呼びます。async/awaitで書くならこんな感じ:const res = await fetch(url); const data = await res.json();
fetchでPOSTリクエストを送るには?
第2引数にオプションを渡します。method: 'POST'、headers(通常は'Content-Type': 'application/json')、そしてbodyを指定。オブジェクトを送るときはJSON.stringify(...)で文字列化が必要です。fetchは自動でシリアライズしてくれないので要注意。
なぜfetchは404や500でrejectされないの?
fetchがrejectされるのはネットワーク障害のときだけです。DNSエラー、接続失敗、CORSブロックなどですね。HTTPのエラーステータス(404や500)はPromise的には「成功」扱いになります。なのでresponse.ok(200〜299でtrue)やresponse.statusを自分でチェックして、エラーなら明示的にthrowする必要があります。
fetchリクエストはキャンセルできますか?
AbortControllerを使えばOKです。コントローラーを作成して、そのsignalをfetchのオプションに渡し、キャンセルしたいタイミングでcontroller.abort()を呼ぶだけ。fetchのPromiseはAbortErrorでrejectされるので、catchで拾って処理できます。