skip to content

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%

answer

  1. NonCancellable = ignores cancel by design
  2. Overuse -> hangs, leaks, detachment
  3. Non-suspend cleanup: use{} / try-finally
  4. Suspend cleanup: tiny + withTimeout
  5. Offload slow finalize; never swallow CancellationException

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.

solid answer

~30 s

NonCancellable defeats cooperative cancellation by design, so wrapping anything beyond minimal cleanup risks unkillable coroutines, delayed shutdown, and resource leaks if the cleanup itself hangs. It also detaches from the parent Job, breaking structured concurrency. Alternatives: for blocking/non-suspending resources, plain try/finally or Closeable.use{} needs no NonCancellable since non-suspend code runs on cancellation anyway. For suspend cleanup, keep the NonCancellable block tiny and wrap it in withTimeout/withTimeoutOrNull so it can't hang. Prefer idempotent, fast release operations; push slow finalization to a separate supervised scope drained at shutdown rather than inline NonCancellable. Always rethrow CancellationException — never catch-and-swallow it as a substitute.

go deeper

for a junior

Recognizes NonCancellable is only for cleanup, not general work.

for a middle

Knows non-suspend cleanup doesn't need it and that suspend cleanup should be bounded.

for a senior

Explains hang/leak/detachment risks and picks use{} vs NonCancellable vs timeout appropriately.

for a principal

Designs a shutdown strategy: bounded inline cleanup vs offloaded drained scope, idempotency, observability, and a team rule to never swallow CancellationException.

## Why overuse is dangerous `NonCancellable` exists to let *small* suspend cleanups finish. Its mechanism — an always-active `Job` that ignores cancellation — is a loaded gun: - **Unkillable coroutines**: any suspend work inside ignores `cancel()`. A long loop or unbounded I/O there can run forever, ignoring shutdown. - **Delayed/blocked shutdown**: `cancelAndJoin()` won't return until the NonCancellable block finishes; a stuck cleanup hangs the whole shutdown path. - **Detachment from structured concurrency**: the block is parented to NonCancellable, so the parent can't tear down children launched there. - **Masked bugs**: people reach for NonCancellable to "make the error go away" instead of fixing where cancellation is observed. ## Cheaper alternatives ### 1. Non-suspending cleanup needs nothing special Non-suspend code in `finally` runs even on cancellation. Use `Closeable.use { }` or plain `try/finally`: ```kotlin file.bufferedReader().use { reader -> process(reader) // close() is synchronous -> runs on cancellation } ``` No `NonCancellable` required. ### 2. Bound the suspend cleanup When cleanup truly suspends, keep it minimal and time-boxed: ```kotlin finally { withContext(NonCancellable) { withTimeoutOrNull(1_000) { conn.closeGracefully() } } } ``` `withTimeoutOrNull` has its own Job/timer, so it can still abort the cleanup. ### 3. Offload slow finalization For expensive finalization, don't block the cancelling coroutine. Enqueue the work to a dedicated `CoroutineScope` (e.g. a supervisor scope you drain during graceful shutdown) so the cancelling path returns promptly: ```kotlin val cleanupScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) // on cancellation: cleanupScope.launch { slowFlush() } ; drain at shutdown ``` ### 4. Never swallow CancellationException Catching `CancellationException` to keep going is a frequent anti-pattern; it breaks cancellation more subtly than NonCancellable. Always rethrow it. ## Decision guide - Cleanup is non-suspending -> `use {}` / `try/finally`, no NonCancellable. - Cleanup suspends, short -> `withContext(NonCancellable)` + timeout. - Cleanup suspends, slow -> offload to a separate drained scope. - Tempted to suppress cancellation broadly -> stop; fix observation instead.

  • A teammate wraps an entire request handler in withContext(NonCancellable) to stop flaky cancellation errors. What's wrong and what do you do?
    It makes the handler unkillable: timeouts and client disconnects no longer stop it, causing hangs and leaks. Remove it; find where cancellation is incorrectly observed (often a swallowed CancellationException or non-cooperative blocking code) and fix that, reserving NonCancellable for short cleanup only.
  • Why is Closeable.use preferable to NonCancellable for closing a file?
    close() is synchronous, so it already runs in finally during cancellation. use {} expresses this cleanly with no cancellation suppression and no detachment risk.

saying these in an interview costs you the question

  • Treats NonCancellable as a general fix for cancellation errors
  • Wraps whole handlers/long work in NonCancellable
  • Uses NonCancellable for synchronous close() that didn't need it
  • No timeout around suspend cleanup
  • Suggests catching/swallowing CancellationException

context