Why does a CoroutineExceptionHandler placed on a child launch have no effect, while the same handler on the scope works?
answer
- Children delegate handling upward; only the root acts
- Handler effective at scope/root, ignored on a child
- Regular Job: child failure cancels parent
- SupervisorJob: direct children are roots for handling
- For local handling use try/catch, not a child handler
basics
~20 sChild coroutines do not handle their own failures; they pass them up to the parent. So a handler set on a child is ignored. Only the handler on the root coroutine or scope, where failures finally land, actually runs.
solid answer
~50 sException handling in structured concurrency is delegated upward. A failing child coroutine first cancels its parent and propagates the throwable up the Job hierarchy; only the **root** coroutine (the one whose parent is the scope's Job) actually decides what to do with an uncaught exception. Therefore the `CoroutineExceptionHandler` is consulted only from the context of that root coroutine — effectively from the scope's context. Installing the handler on an inner `launch(handler) { ... }` that is itself a child is useless: that child won't invoke its own handler; it forwards the exception to its parent. To make the handler effective you put it in the `CoroutineScope(...)` context (or on a top-level/root `launch` directly in the scope). With a `SupervisorJob`, each direct child of the supervisor is treated as a root for handling purposes, so a handler on those children/the supervisor scope can fire.
code
kotlin · 9 linesval handler = CoroutineExceptionHandler { _, e -> println("got $e") }
// No effect: handler on a nested child
CoroutineScope(Job()).launch {
launch(handler) { throw RuntimeException("x") }
}
// Works: handler in the scope context
CoroutineScope(Job() + handler).launch { throw RuntimeException("x") }go deeper
Knows the handler belongs on the scope, not deep inside, even if the reason is fuzzy.
Explains that children delegate to parents and only the root invokes the handler.
Articulates the Job tree, root coroutine concept, and how SupervisorJob redefines the handling boundary.
Designs scope topology so handling lands predictably, choosing supervisor vs regular Job per failure-isolation requirements.
## The rule Kotlin coroutines use **structured concurrency**: every coroutine has a `Job`, and child Jobs form a tree under the scope's Job. When a coroutine fails with a non-`CancellationException`: 1. It cancels its parent (regular `Job`) — bidirectional failure propagation. 2. The exception travels up to the **root** coroutine of that subtree. 3. The root looks up a `CoroutineExceptionHandler` in its context and calls `handleException`. If none is present, the JVM default uncaught handler runs. **Children never handle their own exceptions.** A child delegates the decision to its parent. So a handler attached to a child is never invoked. ## Why placement matters ```kotlin val handler = CoroutineExceptionHandler { _, e -> println("got $e") } val scope = CoroutineScope(Job()) // WRONG: handler on a child has no effect scope.launch { launch(handler) { throw RuntimeException("boom") } // ignored } // RIGHT: handler on the scope (root context) val scope2 = CoroutineScope(Job() + handler) scope2.launch { throw RuntimeException("boom") } // handler runs ``` In the WRONG case, the inner `launch` is a child of the outer `launch`; it forwards the exception upward, and the outer (root) coroutine has no handler — so the default handler runs and the inner `handler` is ignored. ## SupervisorJob nuance With `SupervisorJob`, failure does **not** propagate from child to parent. Each direct child of a supervisor scope is effectively a **root for exception handling**, so a `CoroutineExceptionHandler` in the supervisor scope (or on those direct children) is honored, and one failing child does not cancel its siblings. ## Practical guidance - Put the handler in the **scope context** that owns the root coroutines. - Or attach it to a **top-level launch** that is a direct child of the scope. - Don't bother attaching it to nested children — wrap their bodies in `try/catch` if you need local handling. Key APIs/keywords: `Job`, `SupervisorJob`, `CoroutineScope`, `launch`, `CoroutineExceptionHandler`, structured concurrency, root coroutine.
- How does using SupervisorJob change where the handler is effective?Each direct child of a SupervisorJob acts as a root for handling, so a handler in the supervisor scope (or on those direct children) fires, and a failing child won't cancel its siblings.
- If you need to handle a nested child's failure locally, what do you use?A try/catch around the suspending body of that child; the CoroutineExceptionHandler is not the right tool for in-place recovery.
saying these in an interview costs you the question
- Believing a handler works wherever you attach it
- Not knowing children delegate handling to the parent
- Confusing local try/catch recovery with the global handler
- Ignoring how SupervisorJob changes the root boundary