A coroutine is cancelled; you need a final cleanup suspend call to still run. How do you do that with withContext, and what are the pitfalls?
answer
- Suspend in finally after cancel throws CancellationException
- withContext(NonCancellable) lets cleanup complete
- Combine NonCancellable + Dispatchers.IO if blocking
- Keep it tiny and bounded — it cannot be cancelled
- Never use it to dodge cancellation of real work
basics
~10 sWrap the cleanup in withContext(NonCancellable) inside a finally block. That lets the suspending cleanup run even though the coroutine is already cancelled. Keep it short, because nothing can stop it.
solid answer
~40 sAfter cancellation a coroutine is in a cancelling state, so any further suspension point immediately throws CancellationException — meaning a suspending cleanup in a finally block would not complete. The idiom is withContext(NonCancellable) { ... } inside finally: NonCancellable is a special Job that is never cancelled, so suspend calls inside it run to completion. Pitfalls: keep the block tiny and bounded — it cannot be cancelled, so a hang there hangs forever and breaks cancellation responsiveness; never wrap normal business logic in NonCancellable to dodge cancellation; and combine it with the dispatcher you actually need, e.g. withContext(NonCancellable + Dispatchers.IO) for a blocking close. It is strictly for releasing resources / final writes during teardown.
code
kotlin · 9 linessuspend fun process(stream: Stream) {
try {
stream.consume()
} finally {
withContext(NonCancellable + Dispatchers.IO) {
withTimeout(2_000) { stream.flushAndClose() } // bounded so it can't hang forever
}
}
}go deeper
Recognizes that cleanup must go in finally; may not know suspend cleanup fails after cancel.
Knows withContext(NonCancellable) is the idiom for cleanup during cancellation.
Explains the cancelling-state suspension behavior and bounds the block with timeouts; combines with a dispatcher.
Sets team conventions limiting NonCancellable to bounded teardown and audits for unkillable coroutines.
## The problem When a coroutine is cancelled, it transitions to **cancelling**. From that point, **every suspension point throws `CancellationException`**. So this fails to run its cleanup: ```kotlin try { doWork() } finally { repo.flush() // suspend call -> throws CancellationException, never completes } ``` ## The fix: `NonCancellable` `NonCancellable` is a special `Job` that is **always active and never cancelled**. Running a block under it via `withContext` makes suspension points inside complete normally even during teardown: ```kotlin try { doWork() } finally { withContext(NonCancellable) { repo.flush() // now runs to completion connection.closeSuspending() } } ``` ## Combine with a dispatcher if needed Context elements add up, so you can keep your offload while suppressing cancellation: ```kotlin withContext(NonCancellable + Dispatchers.IO) { blockingClose() } ``` ## Pitfalls - **Keep it minimal and bounded.** Inside `NonCancellable` nothing can interrupt the work — an infinite loop or a network call with no timeout will **hang forever** and defeat cancellation responsiveness. Add explicit timeouts for any I/O. - **Never use it to dodge cancellation for business logic.** It is only for **resource release / final writes** in `finally`/teardown. Wrapping real work in it breaks structured concurrency and makes the coroutine unkillable. - **It does not catch the CancellationException for you.** The original cancellation still propagates after the `finally` completes; `NonCancellable` only lets the cleanup itself finish. ## Why suspension points throw after cancel Kotlin coroutines are **cooperatively cancellable**: cancellation is delivered at suspension points / `ensureActive()` / `isActive` checks. This is what makes a cancelled coroutine stop promptly — and exactly why a normal suspending cleanup needs the `NonCancellable` shield. ## Recap - Cleanup that must run during cancellation → `withContext(NonCancellable) { ... }` in `finally`. - Keep it tiny, bounded (timeouts), resource-release only.
- Why would repo.flush() in a finally block silently not run after cancellation?It is a suspension point; once the coroutine is cancelling, any suspension immediately throws CancellationException, so flush never starts. NonCancellable shields it.
- Does NonCancellable swallow the original cancellation?No. It only allows the wrapped cleanup to complete; the original CancellationException still propagates out after the finally finishes.
- Why add withTimeout inside a NonCancellable block?Because the block itself can't be cancelled, an unbounded I/O call could hang forever; an explicit timeout bounds it.
saying these in an interview costs you the question
- Putting suspend cleanup in finally without NonCancellable and expecting it to run
- Wrapping business logic in NonCancellable to avoid cancellation
- Unbounded blocking call inside NonCancellable (can hang forever)
- Thinking NonCancellable cancels or swallows the original exception
- Believing NonCancellable changes the dispatcher (it does not)