Why doesn’t `withContext` return a value when the ...
# coroutines
k
Why doesn’t
withContext
return a value when the job is canceled? In the Android application, when I call the following code:
Copy code
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
val job = scope.launch {
    val result = withErrorHandlingOnDispatcher(<http://Dispatchers.IO|Dispatchers.IO>) {
        delay(2000)
    }
    println("*** result: $result")
}
scope.launch {
    delay(500)
    job.cancel()
}

suspend fun <T> withErrorHandlingOnDispatcher(
    dispatcher: CoroutineDispatcher,
    doWork: suspend () -> T,
): Result<T> = withContext(dispatcher) {
    try {
        Result.success(doWork())
    } catch (exception: Exception) {
        Result.failure(exception)
    }.apply {
        println("*** withErrorHandlingOnDispatcher result: $this")
    }
}
I get the log:
Copy code
withErrorHandlingOnDispatcher result: Failure(kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@5f15aa2)
which means that
JobCancellationException
is being caught, but
result: $result
is not printed. When I remove
withContext(dispatcher) { }
it prints:
Copy code
withErrorHandlingOnDispatcher result: Failure(kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@9a6574d)
result: Failure(kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@9a6574d)
I guess it’s expected behavior, but why does it work like that? Is there a way to return value from
withContext
anyway?
👀 1
🤔 1
f
Because when a cancelled coroutine is resumed, it is resumed exceptionally with
CancellationException
.In this case
withContext
suspends and resumes the caller coroutine and thus, the original coroutine will not be resumed with the result of withContext, but with CancellationException, which will terminate the coroutine without printing any result. If you want your code to run even when the coroutine is cancelled, then you should wrap everything in a
withContext(NonCancellable)
, making the whole block non-cancellable (even though this is generally a rare requirement) or use can use
try-finally
to make sure that the code in the
finally
block is executed even on a cancelled coroutine.
j
It'd be interesting to know what you're trying to achieve with this
try/catch
k
For the project I joined, every `Retrofit`/`OkHttp` call is wrapped with a bit different version of the
withErrorHandlingOnDispatcher
that returns a custom
Result
type. A custom exception-handling logic is implemented in the
catch
block according to the business requirements. Currently, I’m implementing an upload feature where the user can cancel this process by clicking a button. I keep track of the “upload jobs”, and when the user clicks a button, I
job.cancel()
(
Retrofit
automatically cancels an HTTP call, so the requirement is met). I expected I would get a
JobCancellationException
at the place where I do a call so I would be able to handle it properly (display an error and retry button). However, it seems like my assumption was wrong.
p
Is the same result if you throw a CancellationException() within doWork() block? I think that, is 2 different scenarios when the cancelation comes from the coroutine job in which withContext suspends and when the Cancelation comes from within the withContext block. My assumption was the same as you but maybe the try/catch inside withContext is too late or cannot do anything, the job where it runs has been cancelled, nothing it could do to recover from it
k
Any exception thrown within the
doWork
block is handled correctly, so
CancellationException
is also