Show why a CoroutineExceptionHandler does not catch an exception from async, and where that exception must be handled instead.
answer
- async encapsulates the exception in the Deferred
- Surfaces at await(), not via the handler
- Guard await() with try/catch
- Root async vs child-under-Job differ in propagation
- supervisorScope isolates the async failure until await
basics
~20 sasync stores its failure inside the returned Deferred. The handler only catches failures that nobody is expected to observe. Since you are expected to call await on a Deferred, the exception is rethrown there, and you handle it with try/catch around await.
solid answer
~50 s`CoroutineExceptionHandler` fires only for *uncaught* exceptions from `launch`. `async` returns a `Deferred<T>` and **encapsulates** the exception inside it; the result is meant to be consumed, so the failure is **deferred** until `await()` is called and rethrown there. Because a consumer is expected, the runtime does not treat it as uncaught and does **not** invoke the handler for the async result — you must wrap `await()` in `try/catch`. Subtle point: this is true for a *root* (top-level) `async`. If the `async` is a **child** of another coroutine under a regular `Job`, the failure also propagates structurally to the parent at the moment of failure (cancelling siblings), independent of `await` — but the handler still isn't the consultation point for the Deferred's stored exception. Under a `supervisorScope`, a child async's failure stays isolated until you `await` it.
code
kotlin · 5 linesval handler = CoroutineExceptionHandler { _, e -> println("handler: $e") }
val scope = CoroutineScope(SupervisorJob() + handler)
val d = scope.async { throw IllegalStateException("boom") }
try { d.await() } catch (e: IllegalStateException) { println("caught: ${e.message}") }
// prints: caught: boom (handler line never prints for the async result)go deeper
Knows you handle async errors with try/catch around await.
Explains that the exception is stored in the Deferred and rethrown at await, not sent to the handler.
Distinguishes root vs child async propagation and the supervisorScope isolation case.
Designs concurrent fan-out (coroutineScope/supervisorScope) with explicit await-error policy rather than leaning on a global handler.
## The core difference `launch` is fire-and-forget: its exception has no consumer, so it is **uncaught** and routed to the `CoroutineExceptionHandler` (or default handler). `async` is result-bearing: it returns `Deferred<T>` and the exception is **stored** in that Deferred, to be rethrown at `await()`. ```kotlin val handler = CoroutineExceptionHandler { _, e -> println("handler: $e") } val scope = CoroutineScope(SupervisorJob() + handler) val deferred = scope.async { throw RuntimeException("boom") } // handler is NOT called here try { deferred.await() // exception rethrown HERE } catch (e: RuntimeException) { println("caught at await: ${e.message}") } ``` Output is `caught at await: boom`; the handler line never prints for the async result. ## Why The coroutines machinery decides an exception is 'handled' if there is a place it is **expected** to surface. For `async`, that place is `await()`. So it does not invoke the `CoroutineExceptionHandler` for the Deferred's encapsulated exception — doing so would double-report it. ## Root vs child async - **Root async** (direct child of the scope): exception only surfaces at `await()`. - **Child async under a regular Job**: the failure *also* propagates to the parent immediately (structured concurrency), which can cancel the parent and siblings — even before anyone awaits. The handler is still not the consultation point for the stored result. - **async inside supervisorScope**: the failure is isolated; it surfaces only when you `await` that specific Deferred. ## Practical rule Always guard `await()` with `try/catch` (or wrap the group with `coroutineScope`/`supervisorScope` and catch the rethrow). Do not rely on a `CoroutineExceptionHandler` for `async` results. Key APIs/keywords: `async`, `Deferred`, `await`, `CoroutineExceptionHandler`, `SupervisorJob`, `supervisorScope`, `coroutineScope`, structured concurrency.
- If a child async under a regular Job fails before anyone awaits, what happens?Structured concurrency propagates the failure to the parent immediately, which can cancel the parent and sibling coroutines, even though no one has called await yet.
- How does supervisorScope change async failure behavior?The failure stays isolated to that child; siblings are not cancelled, and the exception only surfaces when you await that specific Deferred.
saying these in an interview costs you the question
- Expecting the handler to catch async failures
- Forgetting to wrap await() in try/catch
- Assuming async never propagates until await even under a regular Job
- Conflating launch and async exception semantics