skip to content

NonCancellable Cleanup

Once a coroutine is cancelled it cannot suspend again, so cleanup that needs to suspend has to run in withContext(NonCancellable). Interviewers pose it as 'your finally block awaits a close call and it never runs'.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

4

Why can a suspending cleanup call inside a finally block fail to run after a coroutine is cancelled, and how does NonCancellable fix it?

level: juniorimportance: must knowfreq 55%

answer

  1. Cancel = throw at next suspend point
  2. finally suspend call -> CancellationException, skipped
  3. Non-suspending finally still runs
  4. withContext(NonCancellable) lets cleanup suspend
  5. Keep it short, cleanup only

basics

~10 s

When a coroutine is cancelled, trying to suspend again (like awaiting a call) in finally immediately throws cancellation, so cleanup is skipped. Wrapping that cleanup in withContext(NonCancellable) lets it suspend and complete.

solid answer

~30 s

Cancellation in Kotlin coroutines works by making suspension points throw CancellationException once the Job is cancelled. A cancelled coroutine is in the Cancelling state, so any suspend call inside a finally block (e.g. delay, an HTTP call, closing a resource that suspends) throws CancellationException immediately and the cleanup never finishes. Non-suspending cleanup in finally still runs. To run suspending cleanup, wrap it in withContext(NonCancellable) { ... }: NonCancellable is a special Job that is always active, so suspension points inside it do not observe the cancellation and the block completes normally before the exception propagates. Keep these blocks short and cleanup-only.

code

kotlin · 14 lines
kotlin
suspend fun example() = coroutineScope {
    val job = launch {
        try {
            repeat(1000) { delay(50) }
        } finally {
            withContext(NonCancellable) {
                delay(100)        // would throw without NonCancellable
                println("cleanup done")
            }
        }
    }
    delay(120)
    job.cancelAndJoin()           // prints "cleanup done"
}

go deeper

for a junior

Knows cancellation throws at suspend points and that withContext(NonCancellable) is the tool for suspend cleanup.

for a middle

Explains the Cancelling state, distinguishes suspend vs non-suspend cleanup, and uses NonCancellable narrowly.

for a senior

Articulates that NonCancellable is an always-active Job and warns against doing real work inside it.

for a principal

Frames it as a structured-concurrency invariant and reasons about timeouts/leaks if cleanup is allowed to be uncancellable indefinitely.

## The problem Kotlin coroutine cancellation is **cooperative** and is delivered at **suspension points**. When you call `job.cancel()`, the coroutine's `Job` moves to the *Cancelling* state. From that moment, every `suspend` call (every point where the coroutine could suspend) checks the job and throws `CancellationException`. A `try/finally` is the normal way to release resources: ```kotlin val job = launch { try { work() } finally { // runs on cancellation too delay(100) // SUSPEND POINT -> throws CancellationException closeAsync() // never reached } } ``` The `finally` block *starts* running on cancellation (good), but the moment it hits a **suspending** call like `delay`, that call sees the job is already cancelled and throws `CancellationException` right away. So suspending cleanup is skipped. Note: **non-suspending** code in `finally` (plain function calls, `close()` on a regular stream) runs fine. Only *suspend* cleanup is affected. ## The fix: withContext(NonCancellable) `NonCancellable` is a singleton `Job` (it implements `Job`) that reports itself as **always active** and ignores cancellation. By switching the coroutine context to it with `withContext`, suspension points inside no longer see the cancellation: ```kotlin val job = launch { try { work() } finally { withContext(NonCancellable) { delay(100) // allowed to suspend closeAsync() // completes } } } ``` Inside the block the coroutine is uncancellable, so the cleanup finishes; afterwards the original `CancellationException` continues to propagate and the coroutine ends as cancelled. ## Key terms - **suspend / suspension point**: a place a coroutine can pause; this is where cancellation is checked and thrown. - **CancellationException**: the signal thrown to unwind a cancelled coroutine; normally swallowed by the machinery. - **Job state Cancelling**: cancellation requested, cleanup running, not yet finished. - **withContext(ctx)**: runs a block in a modified context and returns its result. ## Discipline Use `NonCancellable` **only** for short cleanup/finalization. Doing real work in it defeats cancellation and can hang the system.

  • Does ordinary, non-suspending cleanup in finally need NonCancellable?
    No. Only suspending calls throw on cancellation. Plain code (e.g. a synchronous close()) in finally runs normally.
  • What is NonCancellable, technically?
    A singleton object implementing Job that is permanently active and ignores cancellation; you install it via withContext(NonCancellable).

It's like a fire alarm that cuts the power: you can still walk out (plain code), but you can't take the elevator (suspend) — NonCancellable is the one emergency elevator that still works.

saying these in an interview costs you the question

  • Claims cancellation stops the coroutine instantly without running finally
  • Says you should wrap all of the coroutine body in NonCancellable
  • Thinks NonCancellable starts a new coroutine or thread
  • Believes plain (non-suspend) finally code is also skipped on cancellation

context

open as a page

Show the correct pattern for running a suspending cleanup operation (e.g. releasing a remote lock) after cancellation, and explain why withContext(NonCancellable) is required.

level: middleimportance: must knowfreq 50%

basics

~10 s

Put the suspending cleanup inside finally, and wrap just that cleanup in withContext(NonCancellable). That makes the cleanup ignore the cancellation long enough to finish, then cancellation resumes propagating.

open as a page

How does withContext(NonCancellable) interact with the coroutine context and Job hierarchy? What is and isn't suppressed inside the block?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Inside the block, only the Job is swapped for NonCancellable, so normal cancellation is ignored. The dispatcher and other context stay the same, and timeout-based cancellation (withTimeout) inside the block still works.

open as a page

What are the dangers of overusing NonCancellable, and what alternatives exist for resource cleanup that don't suppress cancellation?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Overusing NonCancellable makes coroutines ignore cancellation, so they can hang shutdown or leak work. For non-suspending cleanup, plain try/finally or use{} is enough; reserve NonCancellable for short suspend cleanup and bound it with a timeout.

open as a page