skip to content

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?

level: seniorimportance: nice to knowfreq 25%

answer

  1. Suspend in finally after cancel throws CancellationException
  2. withContext(NonCancellable) lets cleanup complete
  3. Combine NonCancellable + Dispatchers.IO if blocking
  4. Keep it tiny and bounded — it cannot be cancelled
  5. Never use it to dodge cancellation of real work

basics

~10 s

Wrap 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 s

After 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 lines
kotlin
suspend 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

for a junior

Recognizes that cleanup must go in finally; may not know suspend cleanup fails after cancel.

for a middle

Knows withContext(NonCancellable) is the idiom for cleanup during cancellation.

for a senior

Explains the cancelling-state suspension behavior and bounds the block with timeouts; combines with a dispatcher.

for a principal

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)

context