Q1ドメインが無い URL を await fetch(url) したとき、catch に届く error.name は?
fetch のエラー処理とタイムアウト — AbortController と再試行
fetch が例外を投げない HTTP エラーと、fetch 自身が失敗するネットワークエラーの見分け方、AbortController と setTimeout による時間切れの打ち切り、1 回だけの再試行を扱います。
response.ok を読めば、ファイルが無いという応答は見分けられます。通信が切れていると、ok を読む前の await fetch の行で例外になります。応答が遅いと、次の行へ進まずに待ち続けます。
この記事では、fetch の失敗を種類ごとに例外として受け取る書き方と、AbortController で取得を打ち切る書き方を扱います。
失敗の応答を例外に変える — throw new Error
注文の一覧を、3 つの画面から読み込むとします。前の記事の loadJson のように失敗のときに null を返すと、画面ごとに null かどうかを確かめる必要があり、1 か所でも忘れると、その先で値を読んだ行が止まります。
HTTP エラー(サーバーに届いたが、status が 403 や 404 などの失敗を表す応答)では、fetch は例外を投げません。ok が false のときに throw new Error(...) で投げると、呼び出し側は try / catch の 1 か所で受け取れます。
// ok が false なら、status を添えて例外を投げる
async function loadJson(path) {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${path}`);
}
return await response.json();
}
try {
const orders = await loadJson("fixtures/order.json"); // この行で例外になる
console.log(`注文 ${orders.length} 件`); // 実行されない
} catch (error) {
// 練習問題のコンソールは 403、ほかのサーバーでは 404 など
console.log(error.message); // HTTP 403: fixtures/order.json
}
throw にしておけば、catch を書き忘れた画面でも、止まったときの文言に HTTP 403 が残ります。画面ごとに null を確かめる if を書く代わりに、catch で受け取る場所を決めるだけで済みます。
通信の失敗を区別する — TypeError と instanceof
外出先で電波が途切れると、要求はサーバーに届かず、応答もありません。この失敗も HTTP エラーと同じ catch に届くため、そのままでは「ファイルが無い」と「通信できない」に同じ案内を出してしまいます。
ネットワークエラー(通信が切れている・ドメインが無いなどで、応答が届かない失敗)では、fetch が返す Promise が TypeError で rejected になります。catch で instanceof TypeError を調べれば、自分で投げた Error と区別できます。
// loadJson は前の章と同じ(ok が false なら例外を投げる)
const urls = [
"fixtures/order.json", // サーバーに届くが、ファイルが無い
"https://shop.invalid/orders.json", // .invalid のドメインは、どこにもつながらない
];
for (const url of urls) {
try {
await loadJson(url);
} catch (error) {
// 自分で投げた Error か、fetch が投げた TypeError か
const kind = error instanceof TypeError ? "通信できない" : "応答が失敗";
console.log(`${kind} / ${error.name}: ${error.message}`);
}
}
// 応答が失敗 / Error: HTTP 403: fixtures/order.json
// 通信できない / TypeError: Failed to fetch
- どちらの失敗も、この行で例外になって
catchへ移る catchでerror instanceof TypeErrorを調べて分ける
if (!response.ok)—fixtures/order.jsonは応答が届き、okはfalsethrow new Error(...)— 自分で投げるError
https://shop.invalid/…— 応答が届かないTypeError: Failed to fetch— fetch が投げる
内側の fetch で起きた TypeError は、loadJson の if まで進まずに外へ出ます。どちらの案内を出すかは画面ごとに違うので、loadJson では投げるだけにして、両方が届く呼び出し側の catch で種類を調べます。
通信の失敗の文言はブラウザごとに違う
fetch が投げる TypeError の message は、Chrome では Failed to fetch、Safari では Load failed と、ブラウザで違います。文言の一致で判定すると、ほかのブラウザで別の案内に分かれます。種類は instanceof TypeError で調べます。
時間切れで取得を打ち切る — AbortController
混み合ったサーバーは、応答を返すまでに何十秒もかかることがあります。fetch には待つ時間の上限を指定する引数が無いので、await fetch の行は応答が届くまで進まず、画面は読み込み中のまま止まって見えます。この章のコードでは、応答に 300ms かかる slowFetch を fetch の代わりに使います。
AbortController(fetch を途中で打ち切るオブジェクト)を作り、fetch(url, { signal: controller.signal }) と渡すと、abort() を呼んだときに AbortError で失敗します。
setTimeout が返すタイマー ID(予約を指す数値)を clearTimeout に渡すと取り消せます。
// slowFetch は、実践 3 のコンソールに用意した、応答に 300ms かかる fetch の代わり(使い方は fetch と同じ)
const controller = new AbortController();
// 100ms 後に abort() を呼ぶように予約し、タイマー ID を受け取る
const timerId = setTimeout(() => controller.abort(), 100);
try {
// fetch と同じく、2 番目の引数に signal を入れて渡す
const response = await slowFetch("fixtures/orders.json", { signal: controller.signal });
console.log(response.ok); // 応答は 300ms 後なので、ここには来ない
} catch (error) {
console.log(error.name); // AbortError
} finally {
clearTimeout(timerId); // 応答が先に届いたときのために、予約を取り消す
}
clearTimeout を finally に書くのは、成功でも失敗でも予約を残さないためです。本文を読み終える前に abort() が動くと json() も AbortError で失敗するので、return await response.json() まで try に入れ、本文を読み終えてから finally に進めます。
1 回だけ取得し直す — 再試行と return await
電車で移動中の端末では、通信が一瞬だけ途切れて取得に失敗し、すぐに試し直せば成功することがあります。失敗のたびに利用者へ再読み込みを頼むより、コードの中で自動で取得し直すほうが、画面の表示が途切れません。
再試行(失敗した取得を、同じ URL でもう一度行うこと)は、catch の中でもう一度呼び出して書きます。回数に上限を付けないと、つながらない間ずっと要求を送り続けるので、2 回目の失敗はそのまま呼び出し側へ投げます。
// loadJson は最初の章と同じ。通信の失敗と時間切れのときだけ、1 回だけ取得し直す
async function loadWithRetry(path) {
try {
return await loadJson(path);
} catch (error) {
const retryable = error instanceof TypeError || error.name === "AbortError";
if (!retryable) {
throw error; // 403 や 404 は、送り直しても同じ応答が返る
}
console.log(`${error.name} のため再試行します`); // TypeError のため再試行します
return await loadJson(path); // 2 回目の失敗は、そのまま呼び出し側へ出る
}
}
try {
await loadWithRetry("https://shop.invalid/orders.json");
} catch (error) {
console.log(`2 回とも失敗しました: ${error.name}`); // 2 回とも失敗しました: TypeError
}
fixtures/order.json のような HTTP エラーは、1 回目の catch で投げ直すので、2 回目を呼ばずに呼び出し側へ出ます。下の表は、失敗の種類ごとに、catch での見分け方と再試行するかどうかをまとめたものです。
| 失敗の種類 | catch での見分け方 | 再試行 |
|---|---|---|
| ネットワークエラー(通信が切れた) | error instanceof TypeError が true | 1 回だけする(通信が戻れば成功する) |
| 時間切れ(abort() で打ち切った) | error.name が "AbortError" | 1 回だけする(混雑が収まれば間に合う) |
| ファイルが無い・権限が無い(status 403 や 404) | それ以外(ok を見て自分で投げた Error) | しない(送り直しても同じ応答が返る) |
return だけでは catch を通らない
try { return loadJson(path); } と await を付けずに返すと、Promise を返した時点で try を抜けます。後から rejected になっても catch は動かず、失敗はそのまま呼び出し側へ出ます。return await loadJson(path); と書きます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2100ms 後の abort() を予約し、応答に 300ms かかる取得へ signal を渡すと?
Q3この記事の loadWithRetry が、再試行せずにそのまま投げ直す失敗は?