How does async behave for exceptions under a SupervisorJob, and how is that different from launch?
answer
- Supervisor isolates siblings for both launch and async
- launch error → handler eagerly; async error → stored in Deferred
- async rethrows at await(); handler doesn't apply
- Never await = error silently lost under supervisor
- Regular parent: async failure cancels siblings immediately
basics
~20 sUnder 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 sBoth 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 linessuspend 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
Knows async returns a Deferred and you call await() to get the result.
Explains that async stores its exception for await() while launch reports eagerly, and that supervisor isolates siblings for both.
Adds the silent-loss trap and the regular-parent vs supervisor-parent distinction for async failure timing.
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