Is it necceary to rethrow the CancellationException in kotlin
Asked Answered
E

2

8

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.

Ephedrine answered 16/5, 2023 at 5:58 Comment(1)
Where did you get the information that cancelling a coroutine will cause all other siblings of it to be cancelled as well?Pasquil
R
22

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.

  1. Throwing a CancellationException does not cancel a coroutine.
  2. Catching the 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."

Rebec answered 16/5, 2023 at 8:58 Comment(2)
Can you explain why'd one want to catch a CancellationException, other than for academic purposes?Pasquil
There are two main reasons that you might end up catching a CancellationException. The first is if you want to provide your own handling for all possible errors that might come from the code you're running. This is common in framework-level code, and typically means catching all possible exceptions and then choosing specific ones to rethrow. The second case is if you think that something other than coroutine cancellation might have caused a CancellationException to be thrown. This is much more common than you might think, and I gave some examples in my linked article.Rebec
P
0

Although the best answer has already been given, let me give an example where rethrowing CancellationException makes a difference.

suspend fun main() = coroutineScope {
    val job = launch {
        repeat(5) { i ->
            try {
                println("job: I'm sleeping $i ...")
                delay(100)
            } catch (e: CancellationException) {
                println(e)
                throw e
            }
        }
    }
    delay(150)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}

The result will be as follows. As expected, "I'm sleeping" is not printed after canceling.

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
main: I'm tired of waiting!
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@5384312f
main: Now I can quit.

Now, if you delete throw e, the result will be as follows. The job continues to print "I'm sleeping", even after cancellation, and completes by itself.

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
main: I'm tired of waiting!
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@5384312f
job: I'm sleeping 2 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@5384312f
job: I'm sleeping 3 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@5384312f
job: I'm sleeping 4 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@5384312f
main: Now I can quit.

The above code is a slightly simplified version of the official code in the URL below. https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative

Plains answered 28/6, 2024 at 15:11 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.