Coroutines asyncは例外のハンドリングが結構ややこしくハマりどころだと思います。ハンドリングの方法を間違えるとクラッシュを引き起こすこともあります。
asyncを使う際の例外ハンドリングをまとめていきます。
SupervisorJobやsupervisorScopeなどの細かい解説はしてませんので、最後に載せてる参考リンク等で確認してください。
scope.async
CoroutineScopeから直接asyncを使う場合です。
asyncではawaitしたタイミングで例外が発生するため、awaitの処理をtry catchで囲んでいます。この場合は普通にハンドリングできます。
次に複数のasyncを使った場合です。CoroutineScopeのJobによって挙動が変わります。
この例の場合は、通常のJobを使ってるのでそのスコープから起動したasyncで例外が発生すると、他のasyncの処理が自動でキャンセルされます。
次に SupervisorJob
を使った例です。
この場合は、どれかのasyncがキャンセルしても、他のasyncはキャンセルされません。
scope.launch内でのasync
CoroutineScopeからlaunchにて新たにCoroutineを起動して、その中でasyncを使う場合です。
この場合はtry catchをしていたとしてもクラッシュします。Coroutineは例外が起きた場合に親に伝播します。この例だとasyncで起きた例外が一番上のCoroutineScopeに伝わりクラッシュすることになります。
awaitでtry catchしてる場合は、実はここも呼び出されますが、結局はasyncから親に伝播されてるので、意味がない感じになってしまいます。
最初に説明したscope.asyncの場合が問題ない理由としては、ルートのCoroutineになってるので伝搬する親がいないためです。
supervisorScopeで対応
これを対応するにはいくつか方法があります。まずはよく使われる supervisorScope
で対応する方法です。
supervisorScope
で囲むことで、asyncで起きた例外が親に伝播されることがなくなり、クラッシュを防ぐことができます。この方法がよく紹介されてる気がします。
CoroutineExceptionHandlerで対応
もう一つよくある対応としては CoroutineExceptionHandler
を使う方法があります。
CoroutineExceptionHandler
はキャッチされなかった例外を捕捉することが可能になります。
ただし、CoroutineScope自体がキャンセルされることになるので、対象のスコープは再利用ができなくなります。
coroutineScopeをtry catchで囲む
最後に coroutineScope
を使った方法です。
coroutineScope
を作りそれ全体をtry catchで囲む方法です。これにより親に伝播されることがなくなります。
scope.launch内での複数async
asyncを使うときは単体で使うことは少なく、同時に処理をしたい時が多いと思います。
多くは上のように複数のasyncを使って処理をして結果を待つみたいな感じになると思います。
このとき例外の対応方法によって挙動がどう違うかを見ます。
supervisorScopeで対応
複数asyncを supervisorScope
で対応したときを見てみます。
supervisorScope
の場合は、例外が起きても別の子Coroutineはキャンセルしないために、キャンセルされずに動くことになります。
CoroutineExceptionHandlerで対応
次に CoroutineExceptionHandler
の場合です。
supervisorScope
と異なり CoroutineExceptionHandler
の場合は別の子Coroutineもキャンセルされることになります。
coroutineScopeをtry catchで囲む
最後に coroutineScope
を使った場合です。
こちらも supervisorScope
とは異なり別の子Coroutineがキャンセルされることになります。
大体のケースでは片方のasyncが失敗した場合はもう片方はキャンセルしてほしいと思います。個人的には coroutineScope
を try catchで囲むのがシンプルで良いかなと思っています。