nashcft's blog

時々何か書く。

Kotlin: runCatching と coroutine

内容的には以下の issue で議論されていることの抜粋のようなものだが、つまるところ現状 Kotlin Coroutines と runCatching (より詳細には runCatching の block 内で suspend function を呼んだ場合) の食い合わせが悪い問題に対してどういう対処ができるのかについて、備忘としてまとめておく。

github.com

runCatching は全ての例外を catch する

現時点のコード:

github.com

public inline fun <R> runCatching(block: () -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

runCatchingblock の実行中に発生した例外を全て捕捉し、 Result として返すという振る舞いになっている。 Result.failure(e) は catch した ThrowableResult.Failure で wrap して Result に詰めて返すというもの。

Kotlin Coroutines の cancellation 復習

Kotlin Coroutines では coroutine が cancel された際にシグナルとして現在中断されている箇所で CancellationException を throw し、これが起動中の coroutine 内を通り抜けていく。 runCatching はこの CancellationException も捕捉してしまうので、都度自分で rethrow 処理を実装しないと cancel シグナルの伝搬がそこで止まってしまって上層に cancel されたことが伝わらなくなったり、 cancel 時の振舞いが意図しないものになる可能性があったりといった不都合が出る。

CancellationException の伝搬が上手くいかないとどうなるかについてはそこまで真面目に探していないこともあって今のところ良い感じにまとめられた内容の資料を見つけられていないが、冒頭の issue のコメントや以下のページから雰囲気は掴めると思う。

kotlinlang.org

medium.com

現時点の workaround

じゃあ毎回 CancellationException だけ rethrow するのを書くかというとダルく、特別 cancel された時に実行したい処理を書くなんてことがある場合以外は CancellationException の存在をロジック中で意識させたくないというのが正直なところだと思う。なので現時点では冒頭の issue で提案されているような runCatchingCancellationException の 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 で採用されている。

github.com

ただ冒頭の issue に CancellationException の中でも timeout 由来のもの (TimeoutCancellationException) は別で考慮した方がいいんじゃないか? という意見があり、何をどう catch するかは検討の余地がありそう。

また runCatchingThrowable を catch した所で ensureActive() を呼ぶのはどうかという案も出されているが、 ensureActive() を呼ぶには CoroutineScope, CoroutineContext, または Job のいずれかが見えている必要があり使用に制限が出てくるので、個人的にはあまり筋が良いとは感じない。

メモ