はじめに
Kotlinで例外処理を書く際、runCatching が非常に便利です。
しかしCoroutine内で使用する場合、CancellationExceptionまで捕捉してしまい、キャンセルが上位に伝播しなくなるという問題があります。
本記事ではこの問題を避けるための対応を紹介します。
runCatchingの問題点
CoroutineのキャンセルはCancellationExceptionによって表現されます。
これはエラーというより制御フローの一部であり、親子のCoroutine間で正しく伝播することが重要です。
しかし、以下のようにrunCatchingを使うと問題が発生します。
suspend fun fetch(): Result<String> =
runCatching {
someFunction()
"ok"
}
この処理がキャンセルされた場合、内部で投げられたCancellationExceptionがResult.failureに変換されてしまい、呼び出し元にキャンセルが伝わりません。
結果として、処理が継続したり、タイムアウトが正しく機能しないといった挙動につながります。
方針
対応方針は以下の通りです。
- 通常の例外は
Resultに包んで返す -
CancellationExceptionは捕捉せず、そのまま上位に再スローする
runSuspendCatchingの実装
上記方針を満たすため、以下のヘルパー関数を作成しました。
(GitHubのIssueに記載の方法です)
import kotlinx.coroutines.CancellationException
suspend inline fun <T> runSuspendCatching(
crossinline block: suspend () -> T
): Result<T> =
try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}
これにより、Coroutineがキャンセルされた場合はCancellationExceptionが正しく上位に伝播します。
利用例
runCatchingの代わりに、そのまま置き換えて使用できます。
suspend fun fetch(): Result<String> =
runSuspendCatching {
someFunction(1000)
"ok"
}
ネットワークエラーなどはResult.failureとして扱われ、キャンセル時のみ処理全体が正しく中断されます。
CancellationExceptionを握りつぶした場合の問題
CancellationExceptionはCoroutineの制御を担う例外です。
これを握りつぶすと、以下のような問題が発生しやすくなります。
- 本来停止すべき処理が継続する
- タイムアウトやスコープキャンセルが正しく機能しない
- ログや挙動の追跡が困難になる
おわりに
実は本件は、AIによるコードレビューで判明したものです ![]()
suspend 内で runSuspendCatching を呼び出すことになるため気持ちの良い対応とは言えません。
早く本格対処が進むことを願いします。