When you call async { ... } and the block throws an exception, at what point is that exception observed by the caller?
answer
- async returns Deferred
- exception stored, thrown at await()
- async line itself never throws
- launch reports eagerly, async defers
- deferral != suppression
basics
~10 sThe exception is stored inside the Deferred result and is re-thrown when you call await() on it. Calling async itself does not throw.
solid answer
~40 sasync returns a Deferred<T>. If the coroutine body fails, async catches the exception, completes the Deferred exceptionally, and re-throws it only when you call await(). So `val d = async { error("boom") }` does NOT throw at the async line; `d.await()` throws. This is called exception deferral: async defers the exception into its result holder. Contrast with launch, which has no result to await and reports failures immediately to the parent/CoroutineExceptionHandler. Note that even though await() is where you SEE the exception, the failure of a root async still cancels its parent scope through structured concurrency independently of whether you ever call await().
code
kotlin · 11 linesimport kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async { throw IllegalStateException("boom") }
println("async returned, no throw yet")
try {
deferred.await()
} catch (e: IllegalStateException) {
println("caught at await: ${e.message}")
}
}go deeper
Knows async returns Deferred and the exception surfaces at await(), not at the async line.
Explains the catch-store-rethrow mechanism and contrasts it with launch's eager reporting.
Distinguishes the throwing site (await) from propagation/cancellation through structured concurrency.
Frames it as the future/promise consumption-site model and reasons about API design trade-offs between async and launch.
## What `async` returns `async` is a coroutine builder that returns a `Deferred<T>` — a `Job` that also carries a future result of type `T`. You retrieve that result with the suspending function `await()`. ## Exception deferral When the body of `async` throws, the builder does not propagate the exception immediately. Instead it: 1. Catches the exception. 2. Completes the `Deferred` in a *failed* state, storing the exception. 3. Re-throws the stored exception **only when `await()` is called**. This is why this code prints nothing from the `async` line itself: ```kotlin val deferred = scope.async { throw IllegalStateException("boom") } // no throw here val result = deferred.await() // IllegalStateException thrown HERE ``` ## Why it works this way `async` models a *value-producing* computation. The natural place to surface a failure is where the value is consumed — `await()`. This mirrors futures/promises in other languages. By contrast `launch` is *fire-and-forget*: it returns a plain `Job` with no value, so it cannot defer anything and reports failures to the parent immediately. ## The subtlety: deferral is not suppression Deferral only controls *where you observe* the exception. It does NOT mean the failure is hidden. If the `async` is a direct child of a regular `coroutineScope`/`Job`-based scope, the failure still propagates upward through structured concurrency and cancels siblings, **even if you never call `await()`**. The deferral via `await()` is about the *throwing site for the caller*, not about whether the parent learns of the failure. Key APIs: `async`, `Deferred<T>`, `await()`, `Job`, `coroutineScope`.
- Does launch behave the same way?No. launch returns a Job with no result and no await(), so it reports failures immediately to the parent/CoroutineExceptionHandler rather than deferring them.
- If I never call await(), is the exception gone?Not necessarily. The Deferred stores it, and if the async is a child of a normal scope the failure still propagates and cancels the scope through structured concurrency.
Like a sealed envelope marked 'bad news inside' — you only get hit by the news when you open it (await), but the post office (scope) already knows it was sent.
saying these in an interview costs you the question
- Saying the exception is thrown at the `async { }` call site
- Claiming async swallows the exception entirely
- Confusing async with launch's eager reporting
- Thinking await() must be called for the parent to be cancelled