skip to content

How does async behave for exceptions under a SupervisorJob, and how is that different from launch?

level: middleimportance: should knowfreq 40%

answer

  1. Supervisor isolates siblings for both launch and async
  2. launch error → handler eagerly; async error → stored in Deferred
  3. async rethrows at await(); handler doesn't apply
  4. Never await = error silently lost under supervisor
  5. Regular parent: async failure cancels siblings immediately

basics

~20 s

Under a supervisor, a failing async doesn't cancel its siblings, and it doesn't crash anything on its own — the error waits inside the Deferred until you call await(). With launch, the error surfaces immediately to a handler instead.

solid answer

~40 s

Both launch and async under a SupervisorJob isolate siblings: a failure in one does not cancel the others. The difference is how the exception is delivered. launch is fire-and-forget: its uncaught exception goes to the CoroutineExceptionHandler (or the default handler) at the failure root. async is result-bearing: it stores the exception in the Deferred and rethrows it when you call await(); a CoroutineExceptionHandler does NOT apply to it. So under supervisorScope, a failing async stays dormant until awaited — meaning if you never await, the exception can be silently lost. The idiom is to wrap each await() in try/catch or runCatching so one failed result doesn't break the aggregation. Note: under a regular (non-supervisor) parent, even an async failure propagates upward and cancels siblings as soon as it occurs, independent of await().

code

kotlin · 6 lines
kotlin
suspend fun demo() = supervisorScope {
    val good = async { 1 }
    val bad = async { throw RuntimeException("x") }
    println(good.await())                     // 1
    println(runCatching { bad.await() })      // Failure(...) — contained
}

go deeper

for a junior

Knows async returns a Deferred and you call await() to get the result.

for a middle

Explains that async stores its exception for await() while launch reports eagerly, and that supervisor isolates siblings for both.

for a senior

Adds the silent-loss trap and the regular-parent vs supervisor-parent distinction for async failure timing.

for a principal

Designs aggregation patterns (await + runCatching) and guards against unawaited Deferreds in code review/lint.

## Shared behaviour: siblings are isolated Under a `SupervisorJob`/`supervisorScope`, neither a failing `launch` nor a failing `async` cancels its siblings. That part is identical. ## The difference is delivery of the exception ### launch — fire-and-forget `launch` returns a `Job` and is not expected to produce a value. An uncaught exception is delivered **eagerly** to the failure root's `CoroutineExceptionHandler` (or the thread default). ### async — result-bearing `async` returns a `Deferred<T>`. Its contract is to deliver a **result** via `await()`. So a failure is **stored** in the `Deferred` and **rethrown only when `await()` is called**. A `CoroutineExceptionHandler` is **not** consulted for an async failure. ```kotlin suspend fun loadAll() = supervisorScope { val a = async { fetchA() } // may throw val b = async { fetchB() } // independent val ra = runCatching { a.await() }.getOrNull() // contained val rb = runCatching { b.await() }.getOrNull() ra to rb } ``` ## The silent-loss trap Because an async failure waits for `await()`, **if you never await, the exception can be silently lost** under a supervisor (no handler, nobody throws). Always await every `Deferred` you start, or you risk swallowing errors. ## Supervisor vs regular parent for async - **Supervisor parent:** a failing `async` does **not** cancel siblings; the error is deferred to `await()`. - **Regular parent:** a failing `async` still **propagates upward** at the moment of failure and cancels the parent and siblings — *even before* you call `await()`. (`await()` then also rethrows.) This surprises people who assume async errors only appear at await — that's only true when the async is itself a root, e.g. under a supervisor. ## Practical idiom Wrap each `await()` in `try/catch`/`runCatching` to aggregate partial results, and never leave a `Deferred` unawaited. ## Key APIs - `async` → `Deferred<T>`, `await()` - `launch` → `Job`, `CoroutineExceptionHandler` - `supervisorScope { }`, `SupervisorJob()`, `runCatching`

  • If you never call await() on a failing async under supervisorScope, what happens to the exception?
    It is stored in the Deferred and effectively lost — no handler runs and nothing rethrows it. Always await your Deferreds.
  • Does a failing async under a regular Job wait for await() to affect siblings?
    No. Under a regular parent the failure propagates upward immediately and cancels siblings, independent of await().

launch is a smoke alarm that goes off immediately; async is a sealed letter marked 'open on await' — you only see the bad news when you open it.

saying these in an interview costs you the question

  • Saying async never affects siblings even under a regular Job
  • Claiming CoroutineExceptionHandler catches async failures
  • Forgetting that an unawaited async can silently swallow errors
  • Thinking launch and async deliver exceptions the same way

context