skip to content

Exception Propagation

An uncaught exception in launch travels up the Job hierarchy, cancelling parent and siblings, while CancellationException is treated as ordinary cancellation and stops there. That distinction is the core of every coroutine error-handling question.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

In Kotlin coroutines, what happens when a coroutine started with `launch` throws an uncaught exception?

level: juniorimportance: must knowfreq 70%

answer

  1. launch = fire-and-forget, returns Job
  2. uncaught throw fails the Job
  3. failure cancels parent then siblings
  4. try/catch around launch catches nothing
  5. catch inside the body instead

basics

~10 s

The coroutine fails. The error travels up to its parent, which cancels itself and its other child coroutines, and then the error is reported.

solid answer

~40 s

A `launch` coroutine is fire-and-forget: an uncaught exception immediately fails its `Job`. The failure propagates up the parent-child `Job` hierarchy. The parent is cancelled, which in turn cancels all its other children (the failing coroutine's siblings). The exception is not stored or returned anywhere — once it reaches the root of a regular (non-supervisor) scope, it is delivered to the `CoroutineExceptionHandler` if one is installed, otherwise to the thread's default uncaught-exception handler. The exception is thrown at the point of failure inside the coroutine, not at the `launch` call site, so wrapping `launch { ... }` in try/catch does NOT catch it. You catch it either inside the coroutine body or via a `CoroutineExceptionHandler`.

code

kotlin · 11 lines
kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    val scope = CoroutineScope(Job())
    val sibling = scope.launch {
        try { delay(1000) } finally { println("sibling cancelled") }
    }
    scope.launch { throw IllegalStateException("boom") } // fails
    sibling.join()
    // sibling is cancelled because its sibling failed the shared parent
}

go deeper

for a junior

Knows launch is fire-and-forget and that an uncaught throw is reported rather than returned.

for a middle

Explains that try/catch around launch fails and that handling must happen inside the body.

for a senior

Connects the behavior to the Job tree: parent cancellation then sibling cancellation, plus the reporting step.

for a principal

Frames it as structured concurrency by design and contrasts with supervisor scopes where this propagation is intentionally broken.

## What `launch` is `launch` is a coroutine builder that starts a *fire-and-forget* coroutine and returns a `Job` (a handle to its lifecycle). Unlike `async`, it has no result to return, so it has nowhere to *store* a thrown exception — it treats any uncaught exception as a **failure** of its `Job`. ## Structured concurrency and the Job tree Every coroutine has a `Job`. When you `launch` inside a `CoroutineScope`, the new `Job` becomes a **child** of the scope's `Job`. This forms a tree. The rule for a regular `Job` (not a `SupervisorJob`): **a child's failure fails the parent**. So when a `launch` body throws: 1. The coroutine's `Job` moves to a failed state. 2. It **cancels its parent** with that exception. 3. The parent **cancels all its other children** — the siblings — by throwing `CancellationException` into them. 4. The original exception keeps propagating up until it reaches the scope root, where it is **reported** (to a `CoroutineExceptionHandler` or the default handler). ## Why try/catch around `launch` does not work ```kotlin // WRONG — this catches nothing try { scope.launch { throw IllegalStateException("boom") } } catch (e: Exception) { // never reached: launch returns immediately, // the throw happens later, on another dispatch } ``` `launch { ... }` returns a `Job` *immediately*; the body runs later (possibly on another thread). The exception is thrown deep inside the coroutine machinery, not synchronously at the call site. To handle it you must catch **inside** the body: ```kotlin scope.launch { try { riskyWork() } catch (e: IllegalStateException) { // handled here — no propagation } } ``` ## Reporting vs handling A propagated exception is *reported* at the root: if a `CoroutineExceptionHandler` is in the scope's context it receives the exception; otherwise the JVM's default uncaught-exception handler prints it. (The handler mechanics are a separate topic — here the key point is *that* propagation reaches a reporting point.) ## One subtlety: `CancellationException` If the body throws `CancellationException` (or `kotlinx.coroutines.CancellationException`), that is treated as **normal cancellation**, not a failure — it does not cancel the parent or siblings. Every other exception type is a failure that propagates as above.

  • Why does wrapping `launch { ... }` in try/catch not catch the exception?
    `launch` returns a `Job` synchronously and runs the body later (often on another thread). The throw happens inside the coroutine, not at the call site, so the surrounding try/catch never sees it.
  • How do you actually handle the exception from a launched coroutine?
    Catch it inside the coroutine body, or install a `CoroutineExceptionHandler` in the scope's context to receive propagated, otherwise-uncaught exceptions.

A child throwing a tantrum (failure) ruins the whole family outing — the parent calls it off and pulls the other kids home too.

saying these in an interview costs you the question

  • Claiming try/catch around the launch call catches the body's exception
  • Thinking launch returns the result or stores the exception like async
  • Believing the exception only kills the one coroutine and leaves siblings running
  • Confusing launch with async/await semantics

context

open as a page

How is `CancellationException` treated differently from other exceptions in coroutine propagation?

level: middleimportance: must knowfreq 65%

basics

~10 s

CancellationException means a coroutine was cancelled normally. It does not fail the parent or cancel siblings. Any other exception is a real failure and does propagate up.

open as a page

Given two children launched in the same scope, if one throws, what happens to the other? Walk through the Job-tree mechanics.

level: middleimportance: should knowfreq 55%

basics

~10 s

If one child fails with a real exception, the shared parent is cancelled, and the parent then cancels the other child too. So the sibling stops running.

open as a page

Trace exactly where an uncaught exception ends up when it propagates to the root of a regular (non-supervisor) coroutine scope, and why it cannot be caught at the call site.

level: seniorimportance: should knowfreq 45%

basics

~20 s

The exception bubbles up the parent chain to the top Job. There it is reported — handed to an exception handler if one exists, otherwise to the platform's default handler. It cannot be caught around the launch call because the body runs later.

open as a page

When a parent and several children fail around the same time, which exception is reported at the root, and what happens to the others?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

The first exception that triggers cancellation is the one reported. Later exceptions from coroutines that were cancelled as a result are attached to it as suppressed exceptions, so they aren't lost but aren't reported separately.

open as a page