Coroutines asyncとException

Photo by Andrey Metelev on Unsplash

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 で囲むことで、asyncで起きた例外が親に伝播されることがなくなり、クラッシュを防ぐことができます。この方法がよく紹介されてる気がします。

もう一つよくある対応としては CoroutineExceptionHandler を使う方法があります。

CoroutineExceptionHandler はキャッチされなかった例外を捕捉することが可能になります。

ただし、CoroutineScope自体がキャンセルされることになるので、対象のスコープは再利用ができなくなります。

最後に coroutineScope を使った方法です。

coroutineScope を作りそれ全体をtry catchで囲む方法です。これにより親に伝播されることがなくなります。

scope.launch内での複数async

asyncを使うときは単体で使うことは少なく、同時に処理をしたい時が多いと思います。

多くは上のように複数のasyncを使って処理をして結果を待つみたいな感じになると思います。

このとき例外の対応方法によって挙動がどう違うかを見ます。

複数asyncを supervisorScope で対応したときを見てみます。

supervisorScope の場合は、例外が起きても別の子Coroutineはキャンセルしないために、キャンセルされずに動くことになります。

次に CoroutineExceptionHandler の場合です。

supervisorScope と異なり CoroutineExceptionHandler の場合は別の子Coroutineもキャンセルされることになります。

最後に coroutineScope を使った場合です。

こちらも supervisorScope とは異なり別の子Coroutineがキャンセルされることになります。

大体のケースでは片方のasyncが失敗した場合はもう片方はキャンセルしてほしいと思います。個人的には coroutineScope を try catchで囲むのがシンプルで良いかなと思っています。

参考

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO