Inside coroutineScope you start two async tasks; one throws before you call await(). Does the scope fail, and when do you observe the exception?
answer
- async is a child Job → failure propagates immediately in coroutineScope
- await() = where the stored exception is re-thrown
- Cancellation does NOT wait for await()
- supervisorScope isolates async; observe only via await() try/catch
- Always await every async (or awaitAll)
basics
~20 sYes — because async is still a child of the scope, the failure cancels the scope and its siblings right away. But the actual exception value is re-thrown when you call await() on that task.
solid answer
~40 sIn a `coroutineScope`, `async` attaches to the scope's regular Job just like `launch`. So when an `async` child throws, the failure propagates to the parent **immediately**, cancelling the scope and its sibling children — you don't need to call `await()` for cancellation to happen. What `await()` controls is *where you observe* the exception: the `Deferred` stores it and rethrows it when you call `await()`. If you never await, you may see the failure surface as the `coroutineScope` itself completing exceptionally. This differs from `supervisorScope`, where a failing `async` is isolated and you only see the exception by `try/catch`-ing the `await()`. Key nuance: people assume async 'hides' exceptions until await — true for the *stored value*, false for *structured cancellation* under a regular Job.
code
kotlin · 13 linesimport kotlinx.coroutines.*
suspend fun main() {
try {
coroutineScope {
val a = async { delay(20); throw RuntimeException("A failed") }
val b = async { delay(1000); println("B done") } // cancelled
awaitAll(a, b)
}
} catch (e: Exception) {
println("scope failed with: ${e.message}") // A failed
}
}go deeper
Understands that await() is where an async exception is re-thrown.
Knows async is a child Job and that failures propagate within coroutineScope.
Separates structural cancellation (immediate) from exception observation (at await), and contrasts coroutineScope vs supervisorScope precisely.
Chooses the right batching pattern (awaitAll vs isolated supervisor children) per workload and reasons about partial-failure semantics in production.
## The two-part answer There are two separate questions hiding here: **(1) does the failure trigger structured cancellation?** and **(2) where is the exception value observed?** ## (1) Cancellation happens immediately (regular Job) `async` creates a `Deferred`, which is a `Job`. Inside `coroutineScope`, that Job is a **child of the scope's regular Job**. The structured-concurrency rule applies: an uncaught exception in any child propagates to the parent, which **cancels all siblings** and moves the scope toward failing. This happens **as soon as the async body throws**, regardless of whether you have called `await()` yet. ```kotlin suspend fun demo() = coroutineScope { val a = async { delay(10); throw RuntimeException("A failed") } val b = async { delay(10_000); "B" } // gets cancelled when A fails // even if we never touch a/b, the scope fails because A propagated a.await() } ``` ## (2) await() is where the exception value surfaces The `Deferred` **stores** the exception. Calling `await()` re-throws it. So: - If you `await()` the failing deferred, you get the original exception there. - If you never `await()` it but the failure already cancelled the scope, the `coroutineScope { ... }` call completes exceptionally with that exception. ## Contrast with supervisorScope Under `supervisorScope` (a `SupervisorJob`), a failing `async` is **isolated**: it does NOT cancel siblings and does NOT fail the scope. The exception lives only in that `Deferred`, so the **only** way to observe it is `try { d.await() } catch (e) { ... }`. Forgetting to await there means the failure is effectively swallowed (aside from any installed handler). ```kotlin supervisorScope { val d = async { throw RuntimeException("isolated") } try { d.await() } catch (e: Exception) { println("saw it: ${e.message}") } // sibling work continues regardless } ``` ## Why this trips people up The common myth: 'async defers exceptions until await, so an un-awaited async is harmless.' That's only true for **exception observation** and only fully true under a **SupervisorJob**. Under a regular Job (`coroutineScope`), the failure still tears down the scope structurally. ## Practical guidance - Always `await()` every `async` you start, or use `awaitAll`. - For independent fire-and-forget work where one failure must not kill others, use `supervisorScope` and handle each `await()`. - Prefer `coroutineScope` + `awaitAll(a, b)` for fan-out where any failure should abort the whole batch. ## Summary - coroutineScope: async failure → immediate sibling cancellation + scope failure; await() = where you observe it. - supervisorScope: async failure → isolated; observe only via await() try/catch.
- Under supervisorScope, what happens to an async exception you never await?It stays stored in the Deferred and does not cancel siblings or fail the scope. It is effectively unobserved unless something awaits it; no CoroutineExceptionHandler fires for async.
- How would you fan out N tasks where any failure should abort all of them?Use coroutineScope, start N async tasks, and call awaitAll(...). The first failure cancels the rest and rethrows from awaitAll/the scope.
saying these in an interview costs you the question
- Saying an un-awaited async in coroutineScope is harmless until await
- Claiming async never propagates failures structurally
- Forgetting that supervisorScope isolates the async failure
- Thinking await() is what triggers sibling cancellation
- Believing CoroutineExceptionHandler handles async exceptions