I'm following the "Kotlin coroutine deep dive" book to understand coroutine a bit further. I came across the below statement in the book which I can't understand clearly and would be helpful if someone explain me with a simple example.
CancellationException can be caught using a try-catch, but it is recommended to rethrow it.
And he gave an example,
import kotlinx.coroutines.*
suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
try {
repeat(1_000) { i ->
delay(200)
println("Printing $i")
}
} catch (e: CancellationException) {
println(e)
throw e
}
}
delay(1100)
job.cancelAndJoin()
println("Cancelled successfully")
delay(1000)
}
As you can see, it catches catch (e: CancellationException)
and rethrow it, now I wonder what happens if I don't rethrow it. I commented out the throw e
but the code executes as usual.
As I can see from kotlin doc, CancellationException is used for structured concurrency which means the CancellationException won't cancel the parent instead it signals the parents to cancel all the other children of the parent (Am I right here ?)
I believe that my understanding is wrong and the below code proves that,
import kotlinx.coroutines.*
fun main() = runBlocking {
val parentJob = launch {
launch {
println("Starting child 1")
delay(1000)
throw CancellationException("Close other child under this parent")
}
launch {
println("Starting child 2")
delay(5000)
println("This should not be printed, but getting printed. Don't know why?")
}
delay(2000)
println("Parent coroutine completed")
}
parentJob.join()
}
As you see, I have created a parentJob
and two child jobs inside then I terminate child 1
by throwing CancellationException
and expecting that parent has to cancel child 2
but not.
Short answer: if you must catch a CancellationException
, call ensureActive
afterwards to ensure that the coroutine terminates.
try {
someSuspendingFunction() // throws on error, OR if cancelled
} catch (e: Throwable) { // 🚨 risks catching CancellationException
currentCoroutineContext().ensureActive() // ✅ throws if cancelled
// now handle errors as normal
}
This is a great question that is not easy to answer. There are two important points that will help to understand the explanation. I think these points explain why you are seeing behaviour that you don't expect in the code examples you have given.
CancellationException
does not cancel a coroutine.CancellationException
does not "un-cancel" the coroutine.Cancellation exceptions are used as a "quick exit" mechanism to allow cancelled coroutines to stop what they're doing. However, the exception itself isn't what determines whether the coroutine is cancelled. That information is stored separately as part of the coroutine's Job
.
If you have a cancelled coroutine that generates a cancellation exception, and you catch the exception and don't rethrow it, you can run into issues because the coroutine may not exit even after it's supposed to be cancelled.
Equally, if you throw a cancellation exception in a coroutine that has not been cancelled, you can run into a problem where the coroutine appears to exit silently, without errors, even though an exception was thrown.
Checking for cancellation with the ensureActive
function solves both problems. It checks the status of the Job
itself, and will throw a new CancellationException
if and only if the coroutine has been intentionally cancelled. You can call this function any time you think the current coroutine might have been cancelled, such as in a catch
or runCatching
block.
I've written about this in detail in my article "The Silent Killer That's Crashing Your Coroutines."