skip to content

A loop uses ensureActive() but a broad try/catch around it swallows the exception, so the coroutine never stops. What is happening and how do you fix it correctly?

level: middleimportance: must knowfreq 45%

answer

  1. ensureActive/yield throw CancellationException
  2. catch (Exception) swallows it
  3. CancellationException is a RuntimeException
  4. Always rethrow CancellationException
  5. Or re-check with ensureActive() after catch

basics

~10 s

ensureActive() throws CancellationException, but a catch (e: Exception) swallows it, so the loop keeps running. Fix it by rethrowing CancellationException (or catching only the exceptions you actually expect).

solid answer

~40 s

`ensureActive()` (and `yield()`) signal cancellation by throwing `CancellationException`. A catch-all like `catch (e: Exception)` or `catch (e: Throwable)` captures that exception too and discards it, defeating cooperative cancellation — the loop swallows the stop signal and continues. The correct pattern is to never blanket-swallow: catch only the specific exception you intend to handle, or, if you must use a broad catch, rethrow `CancellationException` first. Note `CancellationException` extends `IllegalStateException` (a `RuntimeException`), so `catch (Exception)` does catch it. Idiom: `catch (e: CancellationException) { throw e }` before other handling, or use `currentCoroutineContext().ensureActive()` after catching to re-detect cancellation. Swallowing it also breaks structured concurrency, since parents rely on the exception to know the child stopped.

code

kotlin · 10 lines
kotlin
while (true) {
    try {
        ensureActive()
        doStep()
    } catch (e: CancellationException) {
        throw e               // propagate cancellation
    } catch (e: Exception) {
        log.error("step failed", e)  // handle real errors only
    }
}

go deeper

for a junior

May not realize a catch-all swallows the cancellation signal.

for a middle

Identifies the swallow, knows CancellationException is an Exception, and rethrows it.

for a senior

Explains structured-concurrency breakage and prefers narrow catches or ensureActive() re-check.

for a principal

Sets a codebase-wide convention/lint to forbid swallowing CancellationException and audits suspend boundaries.

## What goes wrong `ensureActive()` and `yield()` propagate cancellation by **throwing** `CancellationException`. If your loop body is wrapped in a broad handler: ```kotlin while (true) { try { ensureActive() step() } catch (e: Exception) { // BUG: swallows CancellationException log.warn("ignored", e) } } ``` the thrown `CancellationException` is caught and discarded, so the loop never exits. Cancellation is *cooperative*: catching its signal opts you out of cooperating. ## Why a generic catch catches it `CancellationException` extends `IllegalStateException`, which is a `RuntimeException` → `Exception` → `Throwable`. So `catch (Exception)` and `catch (Throwable)` both capture it. ## Correct patterns **1. Rethrow cancellation explicitly:** ```kotlin try { step() } catch (e: CancellationException) { throw e // let cancellation propagate } catch (e: IOException) { handle(e) } ``` **2. Catch only what you expect** (preferred) — don't use a blanket `Exception`/`Throwable` catch around suspendable code. **3. Re-check after a broad catch:** ```kotlin } catch (e: Exception) { currentCoroutineContext().ensureActive() // rethrows if cancelled handle(e) } ``` ## Structured concurrency angle Swallowing `CancellationException` not only keeps the loop alive but also hides the cancellation from parents/`coroutineScope`, breaking the guarantee that a cancelled child stops. The runtime even warns in some cases that exceptions were thrown during cancellation. ## Rule of thumb > In coroutine code, **always rethrow `CancellationException`**; never let a generic catch eat it.

  • Why does catch (e: Exception) catch CancellationException at all?
    CancellationException extends IllegalStateException, a RuntimeException, so it is an Exception and a broad catch captures it.
  • What is a safe alternative to rethrowing manually?
    Call currentCoroutineContext().ensureActive() (or coroutineContext.ensureActive()) inside the catch; it rethrows if the coroutine is cancelled, otherwise lets you handle the real error.

saying these in an interview costs you the question

  • Believing CancellationException is not an Exception subclass
  • Recommending a blanket catch (Throwable) around suspend code
  • Logging-and-continuing on CancellationException
  • Not knowing to rethrow CancellationException
  • Thinking cancellation still works despite the swallow

context