skip to content

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.

level: seniorimportance: should knowfreq 40%

answer

  1. awaitAll = fail-fast, first failure wins
  2. sequential await blocks in order
  3. normal scope cancels siblings on any failure
  4. supervisorScope keeps partial results
  5. runCatching { await() } to isolate

basics

~20 s

awaitAll() 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 s

With 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 lines
kotlin
import 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

for a junior

Knows awaitAll collects results and that a failure surfaces an exception.

for a middle

Explains awaitAll's fail-fast behavior versus sequential await ordering.

for a senior

Integrates scope cancellation timing with await semantics and chooses supervisorScope+runCatching for partial results.

for a principal

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

context