async関数を呼び出したときに呼び出し先でエラーが発生しても処理が止まらない時

開発者ブログ

async/awaitの見落としがちな落とし穴。try/catchしても呼び出し元でcatch出来ません

async/awaitはJavascriptにおける非同期処理のthenという構文をよりわかりやすく記述できる素晴らしい書き方です。とても便利なので多くのジャバスクリプターの方々は利用されていることと思います。

さて、そんな便利なasync / awaitですが、正しい使い方を知っていないと思わぬトラブルが起きることもあります。実際に私がNipoやMaroudを開発している際に、正しい知識を持たずに使ってしまい、意図しない動きをしてしまったケースをご紹介します。

以下のコードは意図しない動きをします

async function func1 () {
  console.log('これはfunc1だよ')
  try {
    const result = await sub()// <ーこの非同期関数がエラーを起こすシナリオ
    console.log('subから非同期でデータをとって、resultに格納したよ', result)// <ー実行されたらまずいやつ
  } catch (e) {
    console.error('func1でエラーだよ', e)
  }
}
async function sub () {
  console.log('subです')
  try {
    const res:string = await dosome() // ここで必ずエラーが発生します。dosome関数は存在しないため
    return res
  } catch (e) {
    console.error('subでエラーだよ', e)
  }
}
  1. 画面にあるボタンをクリックすると、func1関数が実行されます(心の目で見るのです)
  2. func1の中でsub関数を呼び出します。sub関数は非同期処理をします
  3. sub関数のtry内でエラーが発生します
  4. sub関数のcatchに処理が移ります
  5. sub関数の処理が終わり、func1へ戻ります
  6. func1関数はsub関数のエラーを知覚せずに処理を実行します

func1:「止まるんじゃねぇぞ。俺は止まらねぇからよ」と。

いや止まらなきゃだめでしょ

実際に実行したコンソールログはこんな感じになります

async・awaitでエラー時に処理が止まらない

async・awaitでエラー時に処理が止まらないケース。undefinedが格納されたまま処理が続いてしまう

なぜfunc1はエラー発生時に止まらないのか? try・catchを利用しても止まらない理由

async・awaitは便利ですが特にエラー発生時の対応に注意が必要です。async・awaitでエラーが発生しても止まらない原因としてネットで検索すると

const res = hidouki().catch(e => { console.log(e) } )
のような書き方をすると止まらない記事が結構沢山見受けられました。この書き方を私はしないのでよくわかりませんが、これだと呼び出し先(ここではhidouki関数)でエラーが発生してもキャッチされないとかなんとか。
しかしここでは例文の通り try ・ catchを利用しています。sub関数内のエラーでは正しくcatchされますがasync関数の呼び出し元であるfunc1ではキャッチ出来ません
理由は単純で、sub関数がエラーを返していないからです。
func1でもエラーをキャッチしたい場合や、func1で処理を止めたい場合は次のような1行を付け足すことで解決します
async function func1 () {
  console.log('これはfunc1だよ')
  try {
    const result = await sub() // <ーこの非同期関数がエラーを起こすシナリオ
    console.log('subから非同期でデータをとって、resultに格納したよ', result) // <ー実行されたらまずいやつ
  } catch (e) {
    console.error('func1でエラーだよ', e)
  }
}
async function sub () {
  console.log('subです')
  try {
    const res = await dosome() // ここで必ずエラーが発生します。dosome関数は存在しないため
    return res
  } catch (e) {
    console.error('subでエラーだよ', e)
    throw e // ◀この1行を付け足すだけですっ
  }
}

この処理の結果コンソールはこのようになります

async関数の呼び出し元で正しくエラーをキャッチできた

async関数の呼び出し元で正しくエラーをキャッチできた

そもそもawaitの結果はresolveかrejectと考えれば止まらないのも道理

非同期関数の呼び出し元が止まらないのは、sub関数の中でrejectされていないことが原因です。awaitで呼び出した関数はresolveかrejectを返すのですが、sub関数のcatch文の中で何もreturnせず、その後の処理でも結局何も返していないため、resolveと認識されてfunc1は止まらなかったのです。

もともとpromise知ってる人は良いけど、今からJSを勉強する人はこの辺混乱しそうだねー

だから明確に、sub関数の中でエラーが発生したことを呼び出し元に伝えるために、エラーを再送する必要があります。エラーの再送が 「 throw e 」なわけです。

throwをするときは Newするべき!という記事もありますがこれは

throw 'なんかやばいことが起きた'

のように文字だけの場合はだめってことです。throw new Error(e)として更に包んでしまうとErrorオブジェクトのなかにErrorオブジェクトという、過剰包装状態になるので注意です。

errorオブジェクトをnew Errorで包むと扱いにくくなる

errorオブジェクトをnew Errorで包むと扱いにくくなる

 

throw したあとでもfinallyは実行されます

throwを文中に使うとそこで処理が終わり呼び出し元へ処理が戻ります。そこで疑問になるのがfinally句がどうなるのかですが、心配ご無用。ちゃんとfinallyは実行されます

async function func1 () {
  console.log('これはfunc1だよ')
  try {
    const result = await sub() // <ーこの非同期関数がエラーを起こすシナリオ
    console.log('subから非同期でデータをとって、resultに格納したよ', result) // <ー実行されたらまずいやつ
  } catch (e) {
    console.error('func1でエラーだよ', e)
  }
}
async function sub () {
  console.log('subです')
  try {
    const res = await dosome() // ここで必ずエラーが発生します。dosome関数は存在しないため
    return res
  } catch (e) {
    console.error('subでエラーだよ', e)
    throw e // ◀throwされる
  } finally {
    console.log('このファイナリーは実行されるかな???') // ◀ちゃんとじっこうされるよ
  }
}

 

finally句が正常に実行されていることが確認できる

finally句が正常に実行されていることが確認できる

いかがでしたか?私は最初、async関数の中でエラーが発生した時にエラーメッセージをユーザに通知するだけの処理を書いただけで、throwしなかったため、呼び出し先では「正常終了」の通知がユーザに表示されるというプログラムを書いたことがあります。

ユーザから見れば「エラー!失敗です」の直後に「正常終了しました」と相反するメッセージが表示されるため、混乱させてしまったと反省しています。

async・awaitは便利ですが、async関数の中でasync関数を呼び出すといったときは、こういう問題にも気をつけなければなりません。

まとめ

catchの中にはとりあえずthrow eって書いとけばいいのです

あまり適当なことを言わないようにっ。適材適所です

ここまで読んでくれてありがとう
今すぐ無料で始められます
E-mailやパスワード不要の仮アカウントですぐに体験できます。 ※継続的な利用の際は正規アカウントへ変換も可能です
開発者ブログ
クラウド顧客日報管理 無料あり!