非同期のエラーを捕まえる
このレッスンで分かること
rejectされた Promise をawaitすると、その行で例外が投げられるtryとcatchで囲めば、第6章と同じ形で受け止められる- 捕まえ損ねた失敗は、どこにも出ないまま消えることがある
非同期のエラーを捕まえる とは
失敗という返事を返した Promise を、
tryとcatchで受け止める書き方です。第6章で書いた例外処理が、そのまま非同期でも使えます。
失敗という返事もある
第38回で、Promise の返事は 2 通りだと書きました。成功が resolve、失敗が reject です。reject に渡すのは、第6章で作った Error のオブジェクトにします。
function fetchWork(works, id) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const found = works.find(function (item) { return item.id === id; });
if (!found) {
reject(new Error("作品が見つかりません"));
return;
}
resolve(found);
}, 80);
});
}reject を呼んだあとに return を書いているところに注目してください。reject は関数から抜ける構文ではないので、書かないと下の resolve まで届いてしまいます。すでに確定した箱は動かないため実害は出ませんが、読む人には迷子の行に見えます。
await した行で例外になる
reject された Promise を await すると、その行で例外が投げられます。つまり第6章の throw とまったく同じ扱いになります。だから受け止め方も同じです。
async function loadWorkOrFallback(works, id) {
try {
const work = await fetchWork(works, id);
return { ok: true, message: work.title + " を読み込みました" };
} catch (error) {
return { ok: false, message: error.message };
}
}catch が受け取る error は、reject に渡した Error オブジェクトそのものです。error.message で文章を取り出せます。第9章では、この文章をそのまま画面に出して利用者に知らせます。
| 起きたこと | await した行の動き | どこで受ける |
|---|---|---|
resolve された | 中身が変数に入る | 次の行へ進む |
reject された | 例外が投げられる | 同じ try の catch |
| いつまでも確定しない | 再開しない | どこでも受けられない |
3 行目が非同期ならではの落とし穴です。resolve も reject も呼ばれない Promise は、失敗ですらありません。エラーも出ないまま関数が止まったままになります。採点でもこの形は時間切れとして扱われます。
try で囲む範囲
囲むのは await の行だけにするのが基本です。範囲を広げすぎると、通信の失敗と自分の書き間違いが同じ catch に落ちて、どちらなのか分からなくなります。
try {
const work = await fetchWork(works, id);
return { ok: true, message: work.title + " を読み込みました" };
} catch (error) {
return { ok: false, message: error.message };
}上の例では return の行も try の中にありますが、これは受け取った値をすぐ使うためです。work を使った長い処理が続くなら、try の外に出したほうが読みやすくなります。
catchの中でerror.messageを読むときは、rejectに文字列をそのまま渡していないか確かめてください。reject("作品が見つかりません")のように文字列を渡すとerrorは文字列になり、error.messageはundefinedになります。渡すのは必ずErrorのオブジェクトにします。
async の中では throw も使える
new Promise を書かずに、async function の中で直接 throw することもできます。async を付けた関数の中で投げた例外は、その関数が返す Promise の失敗としてそのまま外へ出ます。
async function loadWork(works, id) {
const list = await fetchWorkList(works);
const work = list.find(function (item) { return item.id === id; });
if (!work) {
throw new Error("作品が見つかりません");
}
return work;
}呼ぶ側から見ると、reject された Promise とまったく同じです。await loadWork(...) の行で例外になり、try と catch で受け止められます。
| 書く場所 | 失敗のさせ方 | 受け取り方 |
|---|---|---|
new Promise の中 | reject(new Error(...)) | await した行で例外 |
async function の中 | throw new Error(...) | await した行で例外 |
catch の中 | throw error で投げ直す | さらに外側の catch |
3 行目の投げ直しは、第6章でも扱った形です。自分の階層でできる後片付けだけして、判断は外側に任せたいときに使います。
async function loadWork(works, id) {
try {
return await fetchWork(works, id);
} catch (error) {
console.log("読み込みに失敗しました");
throw error; // 画面に出す判断は呼ぶ側に任せる
}
}finally で後片付けをする
第6章の finally も、そのまま使えます。成功でも失敗でも必ず通るので、読み込み中の表示を消す処理を置くのに向いています。
async function load(works, id) {
showSpinner();
try {
return await fetchWork(works, id);
} catch (error) {
return null;
} finally {
hideSpinner();
}
}finally に置いておけば、成功したときの行にも失敗したときの行にも同じ hideSpinner() を書かずに済みます。次の第42回で、この形をそのまま画面に組み込みます。
よくある間違い
1 つめは、await に try を付け忘れることです。
async function loadWork(works, id) {
const work = await fetchWork(works, id); // 失敗するとここで抜ける
return work.title;
}fetchWork が失敗すると、この関数は例外を投げたまま終わります。呼ぶ側にも try が無ければ、失敗はどこにも出ないまま消えます。画面には何も起きず、原因も分からない、という最悪の形です。
2 つめは、catch の中でさらに失敗する処理を書くことです。
try {
const work = await fetchWork(works, id);
return work.title;
} catch (error) {
return error.response.status; // response が無いと、ここでまた落ちる
}catch の中で落ちた例外を受け止める場所はありません。catch に書くのは、必ず成功する短い処理だけにしてください。
要件
- fetchWork(works, id) を作る。80 ミリ秒後に、id が一致する作品があれば resolve し、無ければ new Error("作品が見つかりません") で reject する
- loadWorkOrFallback(works, id) を async function として作り、fetchWork の await を try で囲む
- 成功したら
{ ok: true, message: タイトル + " を読み込みました" }を返す - 失敗したら catch で受け止め、
{ ok: false, message: error.message }を返す
入出力例
loadWorkOrFallback([{"id":1,"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"id":2,"published":true,"title":"自己紹介カード","views":48,"year":2026},{"id":3,"published":true,"title":"スキル一覧の表","views":32,"year":2026},{"id":4,"published":true,"title":"作品ギャラリー","views":76,"year":2026},{"id":5,"published":false,"title":"学習ログ","views":0,"year":2025}], 1) → {"message":"プロフィールサイト を読み込みました","ok":true}
loadWorkOrFallback([{"id":1,"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"id":2,"published":true,"title":"自己紹介カード","views":48,"year":2026},{"id":3,"published":true,"title":"スキル一覧の表","views":32,"year":2026},{"id":4,"published":true,"title":"作品ギャラリー","views":76,"year":2026},{"id":5,"published":false,"title":"学習ログ","views":0,"year":2025}], 99) → {"message":"作品が見つかりません","ok":false}
loadWorkOrFallback([], 1) → {"message":"作品が見つかりません","ok":false}
loadWorkOrFallback([{"id":1,"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"id":2,"published":true,"title":"自己紹介カード","views":48,"year":2026},{"id":3,"published":true,"title":"スキル一覧の表","views":32,"year":2026},{"id":4,"published":true,"title":"作品ギャラリー","views":76,"year":2026},{"id":5,"published":false,"title":"学習ログ","views":0,"year":2025}], 5) → {"message":"学習ログ を読み込みました","ok":true}