🚧

await/catchパターンの具体的な振る舞い例

2 min read

async関数の絡むエラーハンドリングは、await/catchパターンで書くとスマートになることが古来より知られています。

https://qiita.com/akameco/items/cc73afcdb5ac5d0774bc

ただcatchを書いた時・省いた時、それぞれどう振る舞うのかを個人的によく忘れがちです。
なので備忘録として具体的な振る舞いをメモしておきます。

準備

テスト用として以下のasync関数を定義します。

let userName = undefined;

async function fetchUserName() {
  if (userName != null) {
    return userName;
  } else {
    throw 'User name not found...';
  }
}

ユーザー名をサーバーに問い合わせる想定の関数です。(いくら何でも雑すぎますが、その辺はご容赦…)
ここではuserNameは定義されていなのでこの関数は必ず失敗します。具体的にはrejectされたPromiseオブジェクトが返却されます。

パターン1.catch文を書かない

(async ()=> {
  const userName = await fetchUserName();
  console.log(`Hello ${userName} !`);
})();

catchを省いた時、つまりエラーハンドリングをサボったときです。
userNameが取得できれば挨拶ログが表示されますが、fetchUserNameが失敗すると、そこで処理が止まります。
つまりその先にログ出力まで進みません。

ただ、別にスクリプト全体の処理が止まったりなどの致命的な状態にはならないので、処理を止めるのが想定通りのときは問題無いとも言えます。

パターン2.catch文でreturnを追加してみる

(async ()=> {
  const userName = await fetchUserName().catch(()=> return 'guest'); // ※returnは省略可
  console.log(`Hello ${userName} !`);
})();

catch文で代わりの内容をreturnしたものを追加します。
これでuserNameには代わりにguestの文字列が入り、処理は止まらず最後のログ出力まで進みます。
ログの内容はもちろんHello guest !になります。

パターン3.catch文でthrowしてみる

ではcatch文でまたエラーを投げるとどうなるんでしょう。

(async ()=> {
  const userName = await fetchUserName().catch((err)=> throw err);
  console.log(`Hello ${userName} !`);
})();

これは【catch文を書かない】パターンと一緒で処理が途中で止まり、ログ出力まで進みません。
動作は一緒でも同パターンより明示的なので、処理を止める想定のときはこちらで書いたほうがいいかもしれません。

呼び出し元のasync関数のエラーをハンドリング

fetchUserNameや、上のcatch内throwで書いたエラー内容はどうなったん?と気になる人もいるかと思います。
これはトップレベルの無名即時async関数のほうにエラーが投げられているので、こちらにcatch文を書いて取得することができます。

(async ()=> {
  const userName = await fetchUserName();
  console.log(`Hello ${userName} !`);
})().catch((err)=> {
  console.error(err); // => 'User name not found...' 
});

所感

書き出して見ると、思ったより覚えることはなく、別に失念するレベルでも無い気がしてきました。
しかし非同期処理は複雑になりがちで、ここでエラーが出るとどうなるんだ??って不安になることが多かったのでまとめてみた次第です。