skip to content

What goes wrong if you catch a broad exception type around a suspending call and swallow it? How should you handle CancellationException?

level: middleimportance: must knowfreq 65%

answer

  1. Never swallow CancellationException
  2. catch (e: CancellationException) { throw e } first
  3. it extends IllegalStateException
  4. ensureActive() re-checks after broad catch
  5. swallow = cancellation silently dies

basics

~10 s

CancellationException is how a coroutine learns it was cancelled. If you catch it and don't rethrow, the coroutine thinks the error was handled and keeps running, so cancellation silently stops working.

solid answer

~40 s

Cooperative cancellation works by throwing CancellationException at a suspension point. A broad catch (e: Exception) or catch (e: Throwable) that swallows everything will also catch CancellationException; if you don't rethrow it, you defeat cancellation — the coroutine continues as if nothing happened and may run forever. The rule: never swallow CancellationException. Either don't catch it, or catch it, run minimal cleanup, and rethrow. A common pattern is to rethrow it explicitly: catch (e: CancellationException) { throw e } before a generic handler, or use currentCoroutineContext().ensureActive() after a broad catch. Note CancellationException extends IllegalStateException, so naive type checks can miss it. Cleanup that itself must suspend should run under withContext(NonCancellable) (that mechanism is its own topic).

code

kotlin · 7 lines
kotlin
try {
    process()
} catch (e: CancellationException) {
    throw e          // must rethrow
} catch (e: Exception) {
    log.error("real failure", e)
}

go deeper

for a junior

Knows you shouldn't swallow CancellationException and that delay() can throw it.

for a middle

Explains the swallow-defeats-cancellation mechanism and the rethrow-first pattern; knows the IllegalStateException inheritance.

for a senior

Designs robust try/catch around suspending code, uses ensureActive() and NonCancellable cleanup correctly.

for a principal

Establishes team conventions/lint rules to prevent CancellationException swallowing across a codebase.

## The trap Cooperative cancellation is delivered as a thrown `CancellationException`. Exception handling code can accidentally **eat** it: ```kotlin val job = launch { try { while (true) { doWork() delay(100) // throws CancellationException when cancelled } } catch (e: Exception) { // <-- also catches CancellationException! log.warn("ignored", e) // swallowed -> cancellation defeated } } job.cancelAndJoin() // never returns control as expected; loop continues ``` Because the `catch (e: Exception)` block handles the `CancellationException` and does not rethrow, the coroutine believes the failure was dealt with and **keeps looping**. Cancellation silently stops working. ## Why it's easy to miss `kotlinx.coroutines.CancellationException` (on JVM, a typealias to `java.util.concurrent.CancellationException`) extends `IllegalStateException`, which extends `RuntimeException`/`Exception`. So `catch (e: Exception)`, `catch (e: RuntimeException)`, and even `catch (e: IllegalStateException)` all capture it. ## Correct patterns **1. Rethrow CancellationException first:** ```kotlin try { riskyWork() } catch (e: CancellationException) { throw e // let cancellation propagate } catch (e: Exception) { handle(e) // real errors only } ``` **2. Re-assert active state after a broad catch:** ```kotlin try { riskyWork() } catch (e: Exception) { recordError(e) } currentCoroutineContext().ensureActive() // throws again if cancelled ``` **3. Don't catch at all** unless you have a specific reason. ## Cleanup on cancellation It's fine to catch `CancellationException` to run **cleanup**, as long as you rethrow: ```kotlin try { stream() } catch (e: CancellationException) { closeQuietly(); throw e } ``` If the cleanup itself needs to call suspend functions, the surrounding coroutine is already cancelled, so those calls would immediately throw. Running them under `withContext(NonCancellable) { ... }` lets cleanup complete — that's a sibling topic, but worth naming. ## Summary rule **Never swallow `CancellationException`.** Catch narrowly, rethrow cancellation, and reserve broad catches for genuine failures.

  • Why is catch (e: IllegalStateException) dangerous around suspend calls?
    CancellationException extends IllegalStateException, so such a catch can swallow cancellation unless it rethrows it.
  • How can you recover cancellation after an unavoidable broad catch?
    Call currentCoroutineContext().ensureActive() (or check isActive) afterwards; it rethrows CancellationException if the job was cancelled.

It's like intercepting a 'stop' order, signing for it, and then continuing anyway — the sender thinks it was obeyed but the worker never stops.

saying these in an interview costs you the question

  • catch (e: Exception) {} around suspend code with no rethrow
  • Doesn't know CancellationException subclasses IllegalStateException
  • Logs and swallows cancellation as a normal error
  • Treats CancellationException as a bug to suppress rather than a signal

context