Interview QuestionPractical QuestionFollow-up Questions

Coroutine Cancellation and CancellationException

skydovesJaewoong Eum (skydoves)||7 min read

Coroutine Cancellation and CancellationException

Kotlin Coroutines handle cancellation through a cooperative mechanism built on top of structured concurrency. Cancellation does not stop a coroutine by force. Instead, it propagates a signal through the Job hierarchy, and each coroutine observes that signal at suspension points. How CancellationException fits into this contract is one of the most commonly misunderstood aspects of coroutine error handling. By the end of this lesson, you will be able to:

  • Explain how cancellation propagates downward through the Job hierarchy.
  • Describe how ensureActive() and suspension points make cancellation observable.
  • Explain why CancellationException is treated differently from other exceptions in childCancelled().
  • Identify the consequences of silently swallowing CancellationException.
  • Apply withContext(NonCancellable) correctly for suspending cleanup operations.

The Job Hierarchy and Cancellation Propagation

Structured concurrency organizes coroutines as a parent-child tree. Every coroutine has a Job in its context, and launching a coroutine inside a scope registers the new Job as a child of the scope's Job.

When you call job.cancel(), the runtime calls cancelImpl() internally, which transitions the job to the cancelling state and propagates the signal to all children through cancelChildren():

private fun cancelChildren(cause: CancellationException?) {
    children.forEach { child ->
        child.cancel(cause)
    }
}

Each child receives cancel(), transitions to cancelling, and calls cancelChildren() on its own children. The signal fans out synchronously through the entire subtree. After this step, every Job in the hierarchy is marked as cancelling, but no coroutine has actually stopped executing yet.

Suspension Points and ensureActive()

Cancellation in Kotlin is cooperative, meaning a coroutine must actively observe the cancellation signal. It does this at suspension points through ensureActive():

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

Standard library functions like delay(), yield(), withContext(), and await() all call ensureActive() internally. When the Job is in the cancelling state, isActive returns false and ensureActive() throws CancellationException.

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor