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?
answer
- ensureActive/yield throw CancellationException
- catch (Exception) swallows it
- CancellationException is a RuntimeException
- Always rethrow CancellationException
- Or re-check with ensureActive() after catch
basics
~10 sensureActive() 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 lineswhile (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
May not realize a catch-all swallows the cancellation signal.
Identifies the swallow, knows CancellationException is an Exception, and rethrows it.
Explains structured-concurrency breakage and prefers narrow catches or ensureActive() re-check.
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