How are exceptions handled when an async coroutine throws? When is the exception delivered, and how does the enclosing scope affect propagation?
answer
- async stores exception, await() rethrows
- normal scope: failure cancels parent+siblings even without await
- supervisorScope: isolated, only via await
- GlobalScope.async can swallow errors
- CancellationException is not a failure
basics
~20 sAn exception from async is stored in its Deferred and rethrown when you call await(). But under a normal coroutineScope it also cancels the parent and siblings immediately, so the failure surfaces even before you await.
solid answer
~50 sAn exception thrown inside async is captured in the resulting Deferred and rethrown at the point you call await(). That's the deferred-delivery half. The structured-concurrency half: when async runs in a scope whose Job is a normal (non-supervisor) Job — e.g. inside coroutineScope — the child failure also propagates to the parent immediately, cancelling the parent and all sibling coroutines, independent of whether you ever await. So you can't fully suppress an async failure just by skipping await() in a regular scope. Under supervisorScope (or a CoroutineScope built with a SupervisorJob), a child failure does NOT cancel siblings or the parent; it stays encapsulated in that Deferred and only surfaces via await(). Note GlobalScope.async also doesn't propagate to a parent (there isn't one) — a lost Deferred can swallow the error. CancellationException is special: it's used for normal cooperative cancellation and isn't treated as a real failure.
code
kotlin · 7 linessuspend fun isolated() = supervisorScope {
val ok = async { 1 }
val bad = async { error("x") }
val a = ok.await() // succeeds
val b = runCatching { bad.await() }// failure only here
a to b
}go deeper
Knows await() can throw the exception the async coroutine produced.
Explains that the exception is stored in the Deferred and surfaces at await().
Distinguishes normal vs supervisor scope propagation and notes await can't hide failures under a normal Job.
Designs failure-isolation policy across fan-out (supervisor vs structured), handles CancellationException correctly, and avoids GlobalScope swallowing.
## Two delivery channels When an `async` coroutine throws, the exception travels two ways at once: 1. **Via the result** — it is stored in the `Deferred` and **rethrown when you call `await()`**. 2. **Via the parent Job** — in a *normal* (non-supervisor) scope it **propagates up to the parent immediately**, cancelling the parent and siblings. Whether channel 2 fires depends on the **kind of Job** the scope has. ## Normal scope: `coroutineScope` / plain Job ```kotlin suspend fun demo() = coroutineScope { val d = async { error("boom") } // throws inside // even WITHOUT calling d.await(), this scope is cancelled and rethrows delay(1000) } ``` With an ordinary parent `Job`, a child failure cancels the parent, which cancels every sibling, and the exception propagates out of `coroutineScope`. You **cannot** hide it merely by not awaiting. ## Supervisor scope: `supervisorScope` / `SupervisorJob` ```kotlin suspend fun demo() = supervisorScope { val d = async { error("boom") } // siblings and parent are NOT cancelled here runCatching { d.await() } // exception surfaces ONLY at await() } ``` Under a `SupervisorJob`, child failures are **isolated**: they don't cancel siblings or the parent. The exception lives in the `Deferred` and is delivered solely through `await()`. This is the right tool when each fan-out task should fail independently. ## `GlobalScope.async` There's no structured parent. The failure isn't propagated anywhere; if you drop the `Deferred` without awaiting, the exception is effectively **swallowed**. Avoid this. ## `CancellationException` is special A `CancellationException` signals **normal cooperative cancellation** and is not treated as a failure: it does not propagate to the parent as an error and doesn't trigger handlers like a real exception would. `await()` on a cancelled Deferred throws `CancellationException`. ## CoroutineExceptionHandler note For `async`, a `CoroutineExceptionHandler` is **not** invoked for the result exception — that exception is the caller's responsibility via `await()`. (`launch` is the builder whose uncaught exceptions reach the handler.) ## Key APIs/keywords - `await()` rethrows the stored exception - `coroutineScope` (normal Job) → propagate + cancel siblings - `supervisorScope` / `SupervisorJob` → isolate, surface only via await - `CancellationException` → cooperative cancel, not a failure - `GlobalScope.async` → no parent, can swallow errors
- Can you suppress an async failure by simply never calling await() inside a coroutineScope?No. A normal-Job scope propagates the child failure to the parent and cancels the scope regardless of await.
- Is a CoroutineExceptionHandler invoked for an async's result exception?No. For async, the exception is delivered through await(); the handler applies to uncaught exceptions from launch (root coroutines).
saying these in an interview costs you the question
- Claiming an async exception is fully suppressed by not awaiting in a normal scope
- Thinking supervisorScope still cancels siblings on a child failure
- Believing a CoroutineExceptionHandler catches async result exceptions
- Treating CancellationException as a normal failure exception