内容的には以下の issue で議論されていることの抜粋のようなものだが、つまるところ現状 Kotlin Coroutines と runCatching
(より詳細には runCatching
の block 内で suspend function を呼んだ場合) の食い合わせが悪い問題に対してどういう対処ができるのかについて、備忘としてまとめておく。
runCatching
は全ての例外を catch する
現時点のコード:
public inline fun <R> runCatching(block: () -> R): Result<R> { return try { Result.success(block()) } catch (e: Throwable) { Result.failure(e) } }
runCatching
は block
の実行中に発生した例外を全て捕捉し、 Result
として返すという振る舞いになっている。 Result.failure(e)
は catch した Throwable
を Result.Failure
で wrap して Result
に詰めて返すというもの。
Kotlin Coroutines の cancellation 復習
Kotlin Coroutines では coroutine が cancel された際にシグナルとして現在中断されている箇所で CancellationException
を throw し、これが起動中の coroutine 内を通り抜けていく。 runCatching
はこの CancellationException
も捕捉してしまうので、都度自分で rethrow 処理を実装しないと cancel シグナルの伝搬がそこで止まってしまって上層に cancel されたことが伝わらなくなったり、 cancel 時の振舞いが意図しないものになる可能性があったりといった不都合が出る。
CancellationException
の伝搬が上手くいかないとどうなるかについてはそこまで真面目に探していないこともあって今のところ良い感じにまとめられた内容の資料を見つけられていないが、冒頭の issue のコメントや以下のページから雰囲気は掴めると思う。
現時点の workaround
じゃあ毎回 CancellationException
だけ rethrow するのを書くかというとダルく、特別
cancel された時に実行したい処理を書くなんてことがある場合以外は CancellationException
の存在をロジック中で意識させたくないというのが正直なところだと思う。なので現時点では冒頭の issue で提案されているような runCatching
に CancellationException
の catch/rethrow を追加した関数を定義して使うのが落とし所としては無難だろう:
suspend fun <R> suspendRunCatching(block: suspend () -> R): Result<R> { return try { Result.success(block()) } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { Result.failure(e) } }
このアプローチは実際に android/nowinandroid
で採用されている。
ただ冒頭の issue に CancellationException
の中でも timeout 由来のもの (TimeoutCancellationException
) は別で考慮した方がいいんじゃないか? という意見があり、何をどう catch するかは検討の余地がありそう。
また runCatching
で Throwable
を catch した所で ensureActive()
を呼ぶのはどうかという案も出されているが、 ensureActive()
を呼ぶには CoroutineScope
, CoroutineContext
, または Job
のいずれかが見えている必要があり使用に制限が出てくるので、個人的にはあまり筋が良いとは感じない。