はじめに
Kotlin Coroutines の launch()
, async()
の例外は Job, Deferred にカプセル化されて、join()
, await()
呼び出し先で例外が re-throw されます。
カプセル化された場合でも、親コルーチンに例外伝播は発生するので、子コルーチンとして起動する場合は想定外の動きをしないように注意が必要です。
例外処理
ルートコルーチンの場合
join(),
await() 呼び出し先で、例外を適切に処理する必要があります。
suspend fun waitJob() {
val job = scope.launch { /* 例外が発生しうる処理 */ }
try {
job.join()
// 後続処理
} catch (ex: Exception) {
// エラー処理
}
}
suspend fun fetchResult() {
val deferred = scope.async { /* 例外が発生しうる処理 */ }
try {
val result = deferred.await()
// 結果処理
} catch (ex: Exception) {
// エラー処理
}
}
上記のコードで例外が発生した場合、scope.async()
がルートのコルーチンスコープとなるため、伝播先はないので、await()
に対する try-catch で完結します
子コルーチンの場合
下記のようにコルーチンの中で子として使うと、例外が親に伝播するので、2箇所で例外の対処が発生してしまうことがあります。
意識していないと想定外の動作をしてしまうので注意です。
※ launch-join も同様のため、async-await のみ記述します。
private val handler = CoroutineExceptionHandler { _, th -> handleError(th) }
fun fetchResult() {
uiState.value = UiState.Progress
scope.launch(handler) { // asyncの非同期ブロックの例外が親に伝播してこちらも呼ばれる
val deferred = async { /* MyExceptionが発生しうる処理 */ }
try {
val result = deferred.await()
uiState.value = UiState.Success(result)
} catch (_: MyException) { // await()実行で、asyncの非同期ブロックの例外がキャッチされる
performRecovery()
}
}
}
private fun handleError(throwable: Throwable) {
uiState.value = UiState.Error(throwable) // UIにエラーを通知
}
private fun performRecovery() {
// 例外からの復旧処理
}
上記のコードは、async()
で子コルーチンを立ち上げて、await()
を try-catch していて、MyException が発生しても performRecovery()
で復旧処理を呼び出し、処理が継続することを想定したものです。
しかし、async ブロックで例外が発生すると、親への伝播で handleError()
が呼ばれた上に、await()
の try-catch の performRecovery()
も呼ばれてしまいます。
UIはエラー表示に更新されてしまいます。
どうすべき
シンプルな処理であれば CoroutineExceptionHandler に一任して、Throwable インスタンスの型判定でハンドリングするのが良さそう。
coroutineScope()
, supervisorScope()
を使用する方法もある。
これらは、新たにコルーチンスコープを作り、結果を返すブロッキング関数です。
coroutineScope()
は通常の Job で立ち上げます。
supervisorScope()
は SupervisorJob でスコープを立ち上げるので、親への例外伝播が無くなります。
今回のケースでは、supervisorScope()
を利用して、MyException を親への伝播を防止する。
ルートコルーチンに CoroutineExceptionHandler を持たせて、その他の例外の受け皿にして、UIへのエラー通知をまとめる。
fun fetchResult() {
uiState.value = UiState.Progress
scope.launch(handler) { // asyncの非同期ブロックの例外が親に伝播してこちらも呼ばれる
val deferred = supervisorScope {
async { /* MyExceptionが発生しうる処理 */ }
}
try {
val result = deferred.await()
uiState.value = UiState.Success(result)
} catch (_: MyException) { // await()実行で、asyncの非同期ブロックの例外がキャッチされる
performRecovery()
}
}
}
まとめ
launch()
, async()
のブロック内で発生した例外は、join()
, await()
呼び出し先で re-throw されるだけでなく、親への伝播もあるのでハンドリングに注意が必要。
UIへのエラー通知は、ルートスコープの CoroutineExceptionHandler に任せるのがコードの可読性が高そう。
coroutineScope()
, supervisorScope()
は、新たにコルーチンスコープを作り、結果を返すことができる。
SupervisorJob でスコープを立ち上げる supervisorScope()
なら、親への伝播を防止できる。