非同期のエラーを捕まえる

このレッスンで分かること

  • reject された Promise を await すると、その行で例外が投げられる
  • trycatch で囲めば、第6章と同じ形で受け止められる
  • 捕まえ損ねた失敗は、どこにも出ないまま消えることがある

非同期のエラーを捕まえる とは

失敗という返事を返した Promise を、trycatch で受け止める書き方です。第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 された例外が投げられる同じ trycatch
いつまでも確定しない再開しないどこでも受けられない

3 行目が非同期ならではの落とし穴です。resolvereject も呼ばれない 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.messageundefined になります。渡すのは必ず 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(...) の行で例外になり、trycatch で受け止められます。

書く場所失敗のさせ方受け取り方
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 つめは、awaittry を付け忘れることです。

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 に書くのは、必ず成功する短い処理だけにしてください。

要件

  1. fetchWork(works, id) を作る。80 ミリ秒後に、id が一致する作品があれば resolve し、無ければ new Error("作品が見つかりません") で reject する
  2. loadWorkOrFallback(works, id) を async function として作り、fetchWork の await を try で囲む
  3. 成功したら { ok: true, message: タイトル + " を読み込みました" } を返す
  4. 失敗したら 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}

ヒント

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部
main.js
main.js
学習モード

メモ

非同期のエラーを捕まえる

⌘S で保存