はじめに
コルーチンの例外処理でsuspend関数を呼ぶ場合のアンチパターンを紹介します。
アンチパターン
- 以下のコードの出力結果はどうなるか
suspend fun main() {
val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("A")})
scope.launch {
val deferred = async<Unit> {
delay(100)
throw MyException()
}
try {
val result = deferred.await()
} catch (_: MyException) {
Thread.sleep(100)
println("B")
delay(100)
println("C")
}
println("D")
}
delay(500) // main関数の終了を遅らせる
}
結果は、
B
A
注意しなければならない点
- SupervisorJobを用いても、子コルーチンは孫コルーチンの例外に対してキャンセルが伝播する
-
join()
やawait()
などのコルーチン完了の待機を try-catch すると、例外をキャッチできるが、キャンセル伝播を止めることはできない - catch ブロック内で suspend があると、当該コルーチンはキャンセルされているため中断から戻ってこず、後続処理が実行されない
- 次のような場合はどうなるか
suspend fun main() {
val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("A")})
scope.launch {
val deferred1 = async<Unit> {
delay(300)
throw MyException()
}
try {
val result = deferred1.await()
} catch (_: MyException) {
println("B")
val job = launch {
println("C")
}
job.invokeOnCompletion { println(it) }
}
println("D")
}
delay(500) // main関数の終了を遅らせる
}
結果は、
B
D
kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=StandaloneCoroutine{Cancelling}@6ba26c3c
A
catch ブロック内に suspend が無いため、そのまま後続処理に進んで "D" が出力されました。しかし、catch 内の lauch()
関数によるコルーチン起動は、「Parent job is Cancelling」の理由で即座にキャンセルされてしまい "C" は出力されません。
例外キャッチ後に後続処理を行うためには
親コルーチンへ例外伝播しないようにする必要があります。
【方法1】async 内で例外キャッチする
suspend fun main() {
val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("A")})
scope.launch {
val deferred = async<Unit> {
try {
delay(300)
throw MyException()
} catch (_: MyException) {
println("B")
delay(100)
val job = launch {
println("C")
}
job.invokeOnCompletion { println(it) }
}
}
val result = deferred.await()
println("D")
}
delay(500)
}
B
C
null
D
【方法2】supervisorScope() 関数を使用する
suspend fun main() {
val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("A")})
scope.launch {
val deferred = supervisorScope {
async<Unit> {
delay(300)
throw MyException()
}
}
try {
val result = deferred.await()
} catch (_: MyException) {
println("B")
delay(100)
val job = launch {
println("C")
}
job.invokeOnCompletion { println(it) }
}
println("D")
}
delay(500)
}
B
C
null
D
delay()
による suspend や、launch()
によるコルーチン起動も、どちらも動作するようになりました。