You launch several async tasks where one fails fast. Walk through the timing of exceptions with await() in sequence versus awaitAll(), and how deferral plus scope cancellation interact.
answer
- awaitAll = fail-fast, first failure wins
- sequential await blocks in order
- normal scope cancels siblings on any failure
- supervisorScope keeps partial results
- runCatching { await() } to isolate
basics
~20 sawaitAll() throws as soon as ANY Deferred fails, not in list order. Awaiting one-by-one throws when you reach a failed one. And under a normal scope, a failed async cancels the others before you even await them.
solid answer
~40 sWith several asyncs under a regular coroutineScope: when one fails, structured concurrency immediately cancels the scope and siblings; the original exception propagates out of coroutineScope. If you call d1.await(); d2.await() sequentially, you may block on d1 while d2 has already failed — but the scope cancellation means everything tears down with the original cause regardless. awaitAll(list) is the fail-fast aggregator: it suspends until all complete OR until any one fails, and re-throws the first failure immediately rather than waiting for the others in order. Under supervisorScope, no auto-cancellation happens, so the order in which you await determines which exception you observe first, and other Deferreds keep their results. Use awaitAll for fail-fast fan-out; use supervisorScope + individual runCatching(await) when you want partial results.
code
kotlin · 14 linesimport kotlinx.coroutines.*
// Fail-fast: throws ~100ms, not after the 5s task
suspend fun failFast() = coroutineScope {
val slow = async { delay(5_000); 1 }
val bad = async { delay(100); throw IllegalStateException("boom") }
awaitAll(slow, bad)
}
// Partial results under supervisorScope
suspend fun partial() = supervisorScope {
val ds = List(3) { i -> async { if (i == 1) error("fail $i") else i } }
ds.map { runCatching { it.await() } } // [Success(0), Failure, Success(2)]
}go deeper
Knows awaitAll collects results and that a failure surfaces an exception.
Explains awaitAll's fail-fast behavior versus sequential await ordering.
Integrates scope cancellation timing with await semantics and chooses supervisorScope+runCatching for partial results.
Designs fan-out fault policy (all-or-nothing vs best-effort) and reasons about latency/teardown of cancelled siblings.
## Setup Fan-out: start N asyncs, then collect results. Behavior depends on (a) the scope type and (b) how you await. ## Under a normal `coroutineScope` (Job parent) Structured concurrency dominates. The instant ANY async fails: - the parent scope is cancelled, - all sibling asyncs are cancelled with `CancellationException`, - the original exception propagates out of `coroutineScope`. So whether you write `d1.await(); d2.await()` or `awaitAll(d1, d2)`, a single failure tears the whole scope down with that failure as the cause. The deferral-to-await() detail barely matters because cancellation fires first. ## `await()` in sequence vs `awaitAll()` `awaitAll(vararg)` / `Collection<Deferred>.awaitAll()` is **fail-fast**: it suspends until all complete, but if any one fails it re-throws that failure **immediately**, without waiting for slower siblings in declaration order. Sequential `await()` only throws when you reach the failed one, so you might block on an earlier, still-running task first. ```kotlin import kotlinx.coroutines.* suspend fun fanOut() = coroutineScope { val slow = async { delay(5_000); 1 } val failing = async { delay(100); throw IllegalStateException("boom") } awaitAll(slow, failing) // throws ~100ms in, NOT after 5s } ``` With `slow.await(); failing.await()` you would block ~5s on `slow` first (though under a normal scope the scope is cancelled at ~100ms anyway, so `slow` is cancelled and its await throws CancellationException). ## Under `supervisorScope` No auto-cancellation of siblings. Now the *order you await* decides which exception you see first, and surviving Deferreds still hold their values: ```kotlin suspend fun partial() = supervisorScope { val ok = async { 42 } val bad = async { throw IllegalStateException("boom") } val a = ok.await() // 42, survives val b = runCatching { bad.await() } // failure isolated here a to b } ``` ## Choosing - **Fail-fast all-or-nothing:** `coroutineScope { awaitAll(...) }`. - **Partial results / isolate failures:** `supervisorScope` + per-Deferred `runCatching { await() }`. Key APIs: `async`, `await()`, `awaitAll`, `coroutineScope`, `supervisorScope`, `CancellationException`.
- Under a normal coroutineScope, does awaitAll give you faster failure than sequential await?The observable failure time is similar because scope cancellation fires at the first failure regardless; but awaitAll is the explicit fail-fast aggregator and avoids accidentally blocking on a slow earlier await.
- How do you collect successful results while tolerating some failures?Run the asyncs in a supervisorScope and wrap each await() in runCatching, then filter the Results.
saying these in an interview costs you the question
- Claiming awaitAll waits for all tasks even when one already failed
- Thinking sequential await order changes the outcome under a normal scope (it is cancelled anyway)
- Expecting partial results from a non-supervisor scope
- Using awaitAll and then being surprised siblings were cancelled