Coroutine Cancellation and CancellationException
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
Jobhierarchy. - Describe how
ensureActive()and suspension points make cancellation observable. - Explain why
CancellationExceptionis treated differently from other exceptions inchildCancelled(). - 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