はじめに
以下のコードではtry-catch構文は使用せず、runCatchingを使用しています。文章の中で出てくるrunCatchingはtry-catchと置き換えてもらって大丈夫です。
SupervisorJobを使用しないコード
以下のコードは特にエラーもなく動きます。
withContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext fun main() { runBlocking { runCatching { withContext(Dispatchers.IO) { throw Error() } } println("runBlocking tail") } println("exit") }
runBlocking tail exit
async-await
async-awaitを使用するコードに置き換えた以下のコードはエラーが吐かれます。catchされません。
fun main() { runBlocking { runCatching { async(Dispatchers.IO) { throw Error() }.await() } println("runBlocking tail") } println("exit") }
runBlocking tail Exception in thread "main" java.lang.Error
async-awaitとエラーハンドリング
このような動作になっている理由として考えられるのが、どこでキャッチすればいいかが不明瞭という点があると思います。
asyncの時点で動作は開始していますが、処理の待受は別の場所でできます。
なのでここでの動作としては runBlocking
がエラーをthrowします。
runBlocking { val task = async(Dispatchers.IO) { throw Error() } // ここで実行開始しているからここでthrow? runCatching { // val task = async(Dispatchers.IO) { throw Error() } // ここで定義したとしたら? task.await() // ここでawaitしているからここでthrow? } println("runBlocking tail") }
中でエラーハンドリングをすれば問題なく動作します。
fun main() { runBlocking { async(Dispatchers.IO) { runCatching { throw Error() } }.await() println("runBlocking tail") } println("exit") }
runBlocking tail exit
SupervisorJobを使用する例
これだけでいいのに、何故か SupervisorJob
を使うような例が多く見受けられます。
例としてこのようなものになります。async
を囲んでキャッチしています。
エラーとして出力はされますが、delayされた方の処理は実行されています。
fun main() { runBlocking { val scope = CoroutineScope(SupervisorJob()) scope.launch { runCatching { async { throw Error() }.await() } } scope.launch { delay(200) println("launch 1") } delay(1000) println("runBlocking tail") } }
Exception in thread "DefaultDispatcher-worker-2" java.lang.Error launch 1 runBlocking tail
SupervisorJobは必要か
上記のSupervisorJobを使う例はただ、asyncの中でキャッチされない例外を握りつぶしているだけに見えます。
エラーが起きるとわかっている部分で早期にキャッチするべきであって、このようなSupervisorJobの使い方は適切でないと考えています。
一番最初に書いたこの例も問題はありませんが、withContextの中でエラーをハンドリングすべきだと考えます。
// 良くない? runCatching { withContext(Dispatchers.IO) { throw Error() } } // OK withContext(Dispatchers.IO) { runCatching { throw Error() } }
おわりに
基本的にSupervisorJobを使うことは無いのではないかと思っています。
適切に、そして早期にエラーをハンドリングをしましょう。
そしてasyncをrunCatchingで囲む事はやめましょう。上記の、どこでエラーがthrowされるのかが不明瞭という点を意識すれば、適切でないことはわかりやすいでしょう。