skip to content

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%

answer

  1. try work / finally cleanup
  2. Wrap only the suspend cleanup
  3. withContext(NonCancellable) overrides the Job
  4. Cancellation resumes after the block
  5. Bound with withTimeout(OrNull)

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.

solid answer

~40 s

Structure: do the real work in try, put cleanup in finally, and inside finally wrap any suspending cleanup in withContext(NonCancellable). Without it, the first suspend call in finally (e.g. releaseLock() that does network I/O) throws CancellationException because the coroutine's Job is already in the Cancelling state, leaking the lock. NonCancellable is a Job that is always active, so withContext(NonCancellable) temporarily replaces the cancelled Job in the coroutine context; suspension points inside no longer observe cancellation and complete. After the block, the original CancellationException continues to unwind. Keep the block minimal and ideally guard it with withTimeout so a stuck cleanup can't hang shutdown.

code

kotlin · 10 lines
kotlin
suspend fun useLock(lock: RemoteLock) {
    lock.acquire()
    try {
        doWork()
    } finally {
        withContext(NonCancellable) {
            withTimeoutOrNull(2_000) { lock.release() }
        }
    }
}

go deeper

for a junior

Can place cleanup in finally and knows it needs NonCancellable, even if fuzzy on why.

for a middle

Writes the correct minimal pattern and explains that NonCancellable overrides only the Job so suspend cleanup completes.

for a senior

Adds a timeout guard and discusses leak scenarios and scoping the block tightly.

for a principal

Designs cleanup as a bounded, observable step; considers idempotency, shutdown ordering, and metrics on cleanup duration/timeouts.

## Goal You hold a resource that must be released with a **suspending** call — e.g. releasing a distributed lock over the network, flushing a buffer to a suspending sink, sending a final message. If the coroutine is cancelled mid-work, the release must still happen. ## Wrong ```kotlin try { acquireLock() doWork() } finally { releaseLock() // suspends -> CancellationException -> lock LEAKED } ``` Because cancellation throws at the first suspension point inside `finally`, `releaseLock()` never completes. ## Right ```kotlin try { acquireLock() doWork() } finally { withContext(NonCancellable) { releaseLock() // now allowed to suspend and finish } } ``` ## Why withContext(NonCancellable) works `withContext(ctx)` merges `ctx` into the current coroutine context for the duration of the block. The current `Job` is the *cancelled* one; `NonCancellable` is a `Job` whose `isActive` is always `true` and whose cancellation is a no-op. By passing it to `withContext`, you override the `Job` element of the context inside the block. Suspension points consult that `Job`, see it active, and don't throw. When the block ends, the outer cancelled `Job` is back in scope, so the pending `CancellationException` resumes propagating and the coroutine finishes as cancelled. ## Hardening: bound it Uncancellable code that hangs hangs forever. Guard cleanup with a timeout: ```kotlin finally { withContext(NonCancellable) { withTimeoutOrNull(2_000) { releaseLock() } } } ``` `withTimeoutOrNull` uses its own timing mechanism, so it can still abort the cleanup even though normal cancellation is suppressed. ## Rules of thumb - Scope `NonCancellable` to the **smallest** cleanup section. - Never do business logic or long loops inside it. - Combine with a timeout for I/O cleanup. - Don't catch and swallow `CancellationException` elsewhere — that breaks cancellation, a different but related bug.

  • Can withTimeout still cancel work inside a NonCancellable block?
    Yes. withTimeout/withTimeoutOrNull install their own scope and throw TimeoutCancellationException on the inner block, independent of the outer cancellation that NonCancellable suppresses.
  • What context element does NonCancellable actually replace?
    The Job element of the coroutine context. Dispatcher, name, and other elements are unchanged.

saying these in an interview costs you the question

  • Wrapping the entire try block (or doWork) in NonCancellable
  • No bound on the cleanup, risking a hang
  • Swallowing CancellationException instead of using NonCancellable
  • Claiming NonCancellable changes the dispatcher/thread
  • Releasing the lock outside finally so a thrown exception skips it

context