skip to content

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