skip to content

How does a failure inside a launch block propagate, and what effect does it have on the parent and sibling coroutines?

level: seniorimportance: must knowfreq 60%

answer

  1. launch surfaces failures eagerly (async hides until await)
  2. Child failure cancels parent, which cancels siblings
  3. CancellationException = normal cancel, not a failure
  4. CoroutineExceptionHandler only at the root
  5. join() does not re-throw the failure

basics

~20 s

If a launch block throws an uncaught exception, the failure travels up to its parent. With a normal parent, this cancels the parent and all its other children. The exception is delivered eagerly when it happens.

solid answer

~50 s

An uncaught exception in a launch coroutine is propagated up the Job hierarchy to the parent — launch surfaces failures immediately rather than storing them (unlike async, which holds the exception until await()). With a regular Job parent, the child's failure cancels the parent, which in turn cancels all sibling children (structured concurrency, fail-fast). The exception then reaches the CoroutineExceptionHandler if one is installed in the context, otherwise it goes to the thread's default uncaught-exception handling. To stop one failing child from taking down siblings, you use a SupervisorJob / supervisorScope, where a child failure does not cancel the parent or siblings — but that is a sibling topic. CancellationException is special: it is treated as normal cancellation and does NOT propagate as a failure upward. Note join() does not re-throw the failure to the joiner; propagation happens through the parent-child Job relationship, not through join().

code

kotlin · 9 lines
kotlin
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e -> println("handled: ${e.message}") }
    val scope = CoroutineScope(Job() + handler)
    scope.launch {
        launch { delay(100); println("sibling") } // cancelled by the failure
        launch { error("boom") }                   // fails -> cancels parent + sibling
    }
    Thread.sleep(200) // let it run for the demo
}

go deeper

for a junior

Knows an uncaught exception in launch propagates to the parent and can crash the scope.

for a middle

Explains that a regular-Job parent cancels siblings on a child failure, and that CancellationException is special.

for a senior

Articulates root-only CoroutineExceptionHandler placement, async-vs-launch error timing, and that join() doesn't deliver the failure.

for a principal

Designs blast-radius containment (supervision boundaries, handler placement) and reasons about fail-fast vs isolation trade-offs across a service.

## Where a launch failure goes When the block passed to `launch` throws an **uncaught exception** (anything other than `CancellationException`), the coroutine **fails**. `launch` is an *eager* builder for errors: the failure is surfaced **immediately**, by propagating up the **`Job` hierarchy** to the **parent** coroutine. (Contrast `async`, which *encapsulates* the exception in its `Deferred` and only throws it when you call `await()`.) ## Effect on parent and siblings (regular Job) With a normal parent `Job`, propagation is **fail-fast** and bidirectional: 1. The child failure **cancels the parent**. 2. The parent cancellation **cancels all other children** (siblings). 3. The parent waits for them to finish cancelling, then completes exceptionally with the original cause. This is **structured concurrency**: one failing child tears down the whole scope so you don't leak running work. ```kotlin fun main() = runBlocking { try { coroutineScope { launch { delay(50); println("sibling running") } // gets cancelled launch { delay(10); error("boom") } // fails first } } catch (e: IllegalStateException) { println("caught: ${e.message}") // boom — surfaces at the scope } } ``` ## `CancellationException` is special A `CancellationException` thrown inside a coroutine is treated as **normal cooperative cancellation**, not a failure. It does **not** propagate upward as an error and does not cancel the parent/siblings. This is why you must never swallow `CancellationException` in a generic `catch (e: Exception)` without re-throwing it. ## Where the exception ultimately lands For `launch` (and other 'failure-surfacing' coroutines), once it reaches the **root** of the hierarchy, the exception is delivered to a **`CoroutineExceptionHandler`** if one is installed in the coroutine's context; otherwise it falls through to the platform default (on the JVM, `Thread.UncaughtExceptionHandler`). A `CoroutineExceptionHandler` is only honored at the **root** of a scope (or with `SupervisorJob`), not on an inner child — installing it on a child has no effect because the failure has already propagated past it. ## `join()` does not deliver the failure Failure propagation flows through the **parent-child Job link**, not through `join()`. Calling `job.join()` waits for completion and returns `Unit`; it does **not** re-throw the child's exception to the joiner. So you can't 'catch' a launch failure by wrapping `join()` in try/catch — you must structure the scope or use a handler. ## Containing the blast radius If you don't want one child's failure to cancel siblings, you switch the parent to a **`SupervisorJob`** (via `supervisorScope` or a scope built with `SupervisorJob()`), where child failures are isolated. That mechanism is covered under the coroutineScope-vs-supervisorScope topic; the key point here is that *default* `launch` under a regular `Job` is fail-fast.

  • Why doesn't installing a CoroutineExceptionHandler on an inner child launch catch its exception?
    Because the failure propagates past the child up to the root before any handler runs; the handler is only consulted at the root of the scope (or under a SupervisorJob), so an inner-child handler is ignored.
  • How is launch's error behavior different from async's?
    launch surfaces the exception immediately by propagating to the parent; async encapsulates it in the Deferred and only re-throws it when you call await().

A launch failure under a regular Job is like one diver signaling an emergency: the whole buddy team surfaces together (siblings cancelled) rather than leaving anyone in the water.

saying these in an interview costs you the question

  • Thinking you can catch a launch failure with try/catch around job.join()
  • Claiming a CoroutineExceptionHandler on a child launch will catch its error
  • Treating CancellationException as a propagating failure
  • Saying launch stores the exception like async does
  • Not knowing a child failure cancels siblings under a regular Job

context