Under a SupervisorJob, where must a CoroutineExceptionHandler be installed to actually catch a failing child, and why?
answer
- Handler fires only at the failure ROOT
- Root = no parent OR parent is SupervisorJob
- Nested child handler is ignored — exception propagates up
- Put handler on the supervisor scope or each launched child
- async ignores the handler — use try/catch on await()
basics
~20 sInstall the handler in the context of the failing coroutine itself (or its supervisor scope), not on a nested child. Under a SupervisorJob each child is treated as a top-level coroutine, so its own handler is the one that runs.
solid answer
~50 sA CoroutineExceptionHandler only fires for a coroutine that is the root of its failure — meaning it has no parent to propagate to, or its parent is a SupervisorJob. Under a SupervisorJob, each direct child is effectively a root for exception purposes, so the handler must be in that child's context (passed to its launch builder) or in the supervisor scope's context (inherited by children). A handler installed on a coroutine that is itself a non-root child is ignored, because the exception propagates upward instead of being handled there. Also, the handler works for launch (and the outermost actor), but NOT for async — async stores the exception in the Deferred for await() to throw. So: put the handler at the supervisor boundary or on each launched child; never rely on it for async results.
code
kotlin · 10 linesval handler = CoroutineExceptionHandler { _, e -> println("caught ${e.message}") }
val scope = CoroutineScope(SupervisorJob() + handler)
// fires: child is a root under the supervisor
scope.launch { throw RuntimeException("top") }
// ignored: inner launch is a regular child, propagates up
scope.launch {
launch(handler) { throw RuntimeException("nested") }
}go deeper
Knows a CoroutineExceptionHandler exists and is a last-resort for uncaught errors.
Can place the handler on the scope and knows it doesn't apply to async.
States the root rule (no parent or SupervisorJob parent) and explains why a nested-child handler is ignored.
Designs a consistent error-reporting strategy: handler at supervisor boundaries for telemetry, try/catch at await sites, no silent swallowing.
## What a CoroutineExceptionHandler is A `CoroutineExceptionHandler` is a coroutine-context element (it implements `CoroutineContext.Element`) that acts as the **last-resort handler** for **uncaught** exceptions — the coroutine equivalent of `Thread.uncaughtExceptionHandler`. You add it with the `+` operator to a context. ```kotlin val handler = CoroutineExceptionHandler { _, e -> log.error("uncaught", e) } ``` ## The placement rule The handler is **only invoked for the coroutine that is the *root* of the failure**. A coroutine is a root (for exception handling) when: - it has **no parent** (a top-level coroutine in a scope), **or** - its parent is a **SupervisorJob** (so the failure is not propagated upward). If a coroutine is a **regular child**, its uncaught exception is **propagated to the parent**, not handled locally — so a handler installed on that inner child is simply **ignored**. ## Why placement matters under a SupervisorJob Because a `SupervisorJob` stops upward propagation, **each direct child becomes a root** for exception purposes. So the handler must be reachable by that child's context. Two correct placements: 1. **On the supervisor scope** — children inherit it through the context. 2. **On each launched child** — passed directly to `launch(handler) { }`. ```kotlin val handler = CoroutineExceptionHandler { _, e -> println("caught: ${e.message}") } // Correct: handler on the supervisor scope, inherited by the child root val scope = CoroutineScope(SupervisorJob() + handler) scope.launch { throw RuntimeException("boom") } // handler fires // Wrong: handler on a NESTED child scope.launch { launch(handler) { throw RuntimeException("boom") } // ignored! } // The inner launch is a regular child of the outer launch (a normal Job), // so its exception propagates up, bypassing the inner handler. ``` ## async is different A `CoroutineExceptionHandler` does **not** apply to exceptions from `async`. `async` is *expected* to expose its result via `await()`, so it **stores** the exception in the `Deferred` and rethrows it at `await()`. Use `try/catch` around `await()` (or `runCatching`) instead. ## supervisorScope and handlers Inside `supervisorScope { }`, a directly-`launch`-ed child is a root, so a handler in the scope context will catch it. But note: the supervisorScope **block body itself** rethrows on the calling coroutine, where a different (or no) handler applies. ## Key APIs - `CoroutineExceptionHandler { ctx, throwable -> }` - `SupervisorJob()`, `supervisorScope { }` - `launch(context)`, `async`/`await`, `Deferred`
- Why doesn't a CoroutineExceptionHandler catch exceptions from async?async is meant to deliver results (and errors) through await(); it stores the exception in the Deferred so the consumer can handle it at await(). The handler is reserved for fire-and-forget launch.
- If you put a handler on an inner nested launch under a regular Job, does it run?No. The inner coroutine is a regular child, so its exception propagates to the parent; the handler is ignored. It only runs at a root.
The handler is like a building's ground-floor reception: complaints are only handled at the entrance (the root), not at an interior office that just forwards them upstairs.
saying these in an interview costs you the question
- Installing the handler on a nested child and expecting it to fire
- Believing CoroutineExceptionHandler catches async failures
- Not knowing that the handler only runs at the failure root
- Thinking try/catch inside the coroutine and the handler are equivalent in all cases