nowinandroid icon indicating copy to clipboard operation
nowinandroid copied to clipboard

Calling ensureActive instead of re-throwing CancellationException in suspendRunCatching

Open azabost opened this issue 8 months ago • 5 comments

In suspendRunCatching, a CancellationException gets re-thrown:

https://github.com/android/nowinandroid/blob/689ef92e41427ab70f82e2c9fe59755441deae92/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/SyncUtilities.kt#L58

However, according to this post (further acknowledged here), it's more advisable to ensureActive() instead. Maybe like this?

private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> = try {
    Result.success(block())
} catch (cancellationException: CancellationException) {
    currentCoroutineContext().ensureActive() // if _our_ coroutine was cancelled - respect the cancellation
    Result.failure(cancellationException) // otherwise, just treat it as a failure
} catch (exception: Exception) {
    Log.i(
        "suspendRunCatching",
        "Failed to evaluate a suspendRunCatchingBlock. Returning failure Result",
        exception,
    )
    Result.failure(exception)
}

What do you think?

azabost avatar May 20 '25 00:05 azabost

I think the current code is fine.

The passing cancelation throw is the basic concept of coroutine in structured concurrency.

If you think it is silent cancel. You can insert log before throw exception.

What do you think of it?

Jaehwa-Noh avatar Aug 19 '25 11:08 Jaehwa-Noh

The passing cancelation throw is the basic concept of coroutine in structured concurrency.

Have you read the linked article, though? Its subtitle literally says: "There’s only one safe way to deal with cancellation exceptions in Kotlin, and it’s not to re-throw them" (emphasis added), suggesting that the current code, which simply rethrows, could be improved.

What do you think?

azabost avatar Aug 19 '25 13:08 azabost

I just share my opinion. I see that article briefly and search that ensureActiviry source code.

That source code also throws cancellation.

public fun Job.ensureActive(): Unit {
    if (!isActive) throw getCancellationException()
}

And this project code using for a sync worker and throw cancellation will occur Result.retry()

Jaehwa-Noh avatar Aug 20 '25 00:08 Jaehwa-Noh

Here's the difference between what you currently have and what I propose based on the linked article:

Before After
Image Image

azabost avatar Aug 21 '25 13:08 azabost

When in after case is normally end the Job. So that parent coroutine doesn't cancel any other child jobs.

When the throw occurs in a child firstly and it goes up to parent case, ensureActive always doesn't throw cancellationException because parent is not canceled that moment.

Jaehwa-Noh avatar Aug 22 '25 02:08 Jaehwa-Noh