Coroutine Cancellation & Failure
How a coroutine tree stops and how failure propagates — the Job hierarchy, cooperative cancellation and yield, SupervisorJob and supervisorScope, where CoroutineExceptionHandler actually catches, and cancellation-safe cleanup with finally and NonCancellable.
9 questions
JuniorTheoryVery commonWhat happens to a coroutine's children when its Job is cancelled?
What happens to a coroutine's children when its Job is cancelled?
Cancelling a Job cancels its whole subtree — every child is cancelled too, and the parent is not completed until they have all finished. A Job is a lifecycle handle, not a result: it reports active, cancelled or done, and carries no value.
Common mistakes
- ✗Thinking a child coroutine survives the cancellation of its parent
- ✗Treating a
Jobas a holder of the result rather than a lifecycle handle - ✗Assuming a cancelled parent is completed immediately, before its children stop
Follow-up questions
- →What does
cancelAndJoin()add over a barecancel()? - →How does a
Deferreddiffer from a plainJobhere?
JuniorTheoryCommonWhat does yield() do inside a coroutine, and when do you need it?
What does yield() do inside a coroutine, and when do you need it?
yield() is a suspension point: it gives the dispatcher a chance to run other coroutines, and on resume it throws CancellationException if the coroutine has been cancelled. A long loop that never suspends needs it — otherwise it never observes cancellation.
Common mistakes
- ✗Believing a cancelled coroutine is stopped by force wherever it happens to be
- ✗Thinking
yield()blocks the thread instead of suspending the coroutine - ✗Assuming a loop checks cancellation on its own, with no suspension point
Follow-up questions
- →How does
ensureActive()differ fromyield()inside a tight loop? - →What does
isActivereport right aftercancel()has been called?
MiddleDebuggingCommonWhy is this CoroutineExceptionHandler never called on the failure?
Why is this CoroutineExceptionHandler never called on the failure?
A handler fires only on the coroutine that handles the failure — the scope's root. On a child it is ignored: the exception is passed up to the parent. Install it in the scope's context. async never calls it — await() rethrows.
Common mistakes
- ✗Installing the handler on a child coroutine instead of the scope's root
- ✗Expecting a handler to fire for a failure raised inside
async - ✗Thinking a
try/catcharoundlaunchcatches what the child throws
Follow-up questions
- →What does a
SupervisorJobchange about which coroutine handles the failure? - →Where does the exception go when no handler is installed at all?
MiddleTheoryCommonHow do coroutineScope and supervisorScope differ when one child fails?
How do coroutineScope and supervisorScope differ when one child fails?
Both suspend until every child has finished. Under coroutineScope a failing child cancels its siblings and the block rethrows that failure. Under supervisorScope the failure stays with that child: the siblings keep running and a handler reports it.
Common mistakes
- ✗Thinking
coroutineScopedoes not wait for its children - ✗Expecting a sibling to be cancelled by a failure under
supervisorScope - ✗Assuming
supervisorScopeswallows the child's failure entirely
Follow-up questions
- →Which of the two fits several independent sections of one screen?
- →What happens when the
supervisorScopebody itself throws?
MiddleTheoryCommonHow does a SupervisorJob change the way a child's failure propagates?
How does a SupervisorJob change the way a child's failure propagates?
Under a regular Job a failing child cancels its parent, which then cancels every sibling. A SupervisorJob lets failure travel downwards only: the failed child dies alone, its siblings keep running. Cancelling the parent still cancels every child.
Common mistakes
- ✗Thinking a
SupervisorJobswallows the failure instead of isolating it - ✗Expecting the siblings to die too when one child under a supervisor fails
- ✗Believing that cancelling a supervisor leaves its children running
Follow-up questions
- →Where must the
SupervisorJobbe installed for that isolation to work? - →Who then reports the failure of a child under a
SupervisorJob?
MiddleDebuggingOccasionalWhy does this filter keep burning CPU after the screen has been closed?
Why does this filter keep burning CPU after the screen has been closed?
Cancellation is cooperative: it lands only at a suspension point or an explicit check, and this loop has neither, so it runs to the end. Check it every iteration with ensureActive() or yield(), or guard the loop with while (isActive).
Common mistakes
- ✗Assuming a cancelled coroutine is force-stopped at an arbitrary point
- ✗Thinking the dispatcher, not a suspension point, is what makes cancellation land
- ✗Believing a CPU loop is interrupted between its iterations for free
Follow-up questions
- →How does
ensureActive()differ from readingisActivein this loop? - →What would
yield()add here beyond the cancellation check?
MiddleDesignOccasionalA repository opens a temp file and a database transaction inside a coroutine and closes both in a finally block. Both close calls are suspend functions. When the user leaves the screen the scope is cancelled — and you find that the finally block does run, but the two suspending close calls never do: the temp files pile up and the transaction is left open. Explain how cancellation interacts with a finally block in a coroutine, why a suspending call inside it fails, and how you would write that cleanup so it always completes. State the limits you would put on the cleanup you propose.
A repository opens a temp file and a database transaction inside a coroutine and closes both in a finally block. Both close calls are suspend functions. When the user leaves the screen the scope is cancelled — and you find that the finally block does run, but the two suspending close calls never do: the temp files pile up and the transaction is left open. Explain how cancellation interacts with a finally block in a coroutine, why a suspending call inside it fails, and how you would write that cleanup so it always completes. State the limits you would put on the cleanup you propose.
A cancelled coroutine's Job is no longer active, so any suspending call inside finally fails at once with CancellationException and the cleanup never runs. Wrap it in withContext(NonCancellable) — and keep it short and bounded, because nothing can stop it.
Common mistakes
- ✗Thinking
finallyis skipped altogether when a coroutine is cancelled - ✗Expecting a suspending call to work normally inside a cancelled coroutine
- ✗Putting long or unbounded work inside a
NonCancellableblock
Follow-up questions
- →Why must a
NonCancellablesection be kept short and bounded? - →Which cleanup calls need no
NonCancellableat all?
SeniorDesignOccasionalA dashboard ViewModel loads five independent widgets — profile, feed, notifications, billing, promos — each with its own suspending repository call, all launched in viewModelScope. Today one flaky widget takes the whole screen down: its failure cancels the other four and the user sees a blank error page. Product wants each widget to fail on its own, showing an error inside its own card while the rest of the dashboard stays usable — and leaving the user able to close the screen and cancel all five at once. Describe the scope and failure handling you would build, where each failure is observed, and why a try/catch wrapped around launch does not solve this.
A dashboard ViewModel loads five independent widgets — profile, feed, notifications, billing, promos — each with its own suspending repository call, all launched in viewModelScope. Today one flaky widget takes the whole screen down: its failure cancels the other four and the user sees a blank error page. Product wants each widget to fail on its own, showing an error inside its own card while the rest of the dashboard stays usable — and leaving the user able to close the screen and cancel all five at once. Describe the scope and failure handling you would build, where each failure is observed, and why a try/catch wrapped around launch does not solve this.
Give the widgets a supervisor — supervisorScope or a SupervisorJob in the scope — so failure travels only downwards and cannot cancel the siblings. Catch inside each child and map the error onto its card: a try around launch catches nothing, as the child fails later.
Common mistakes
- ✗Expecting a
tryaroundlaunchto catch what the child throws - ✗Attaching a
CoroutineExceptionHandlerto a child and expecting it to fire - ✗Reaching for
GlobalScopeto isolate failures, and losing cancellation with it
Follow-up questions
- →What still cancels all five widgets at once under that supervisor?
- →Where would you place the retry for a single failed widget?
SeniorDebuggingOccasionalWhy does leaving the screen log a network error and leave the temp file?
Why does leaving the screen log a network error and leave the temp file?
Cancellation arrives as CancellationException, which catch (e: Exception) swallows — so a normal cancel is logged as a network failure. The suspending deleteTemp() in finally then fails at once, as the Job is cancelled. Rethrow it; clean up in NonCancellable.
Common mistakes
- ✗Catching
Exception(or usingrunCatching) around a suspending call and swallowing the cancel - ✗Reporting a
CancellationExceptionto the user as if it were a real failure - ✗Expecting a suspending cleanup in
finallyto run inside a cancelled coroutine
Follow-up questions
- →Why is
runCatchingeven more dangerous here thancatch (e: Exception)? - →Which failures should this download's
catchstill handle itself?