skip to content

You fan out N independent network calls and want partial results even if some fail. Which scope builder do you use, and how do you collect results safely?

level: seniorimportance: should knowfreq 50%

answer

  1. independent tasks -> supervisorScope
  2. async + await each inside try/catch
  3. async failure surfaces at await()
  4. rethrow CancellationException, don't swallow
  5. coroutineScope would cancel all on first failure

basics

~10 s

Use supervisorScope so one failing call doesn't cancel the others. Launch each call with async, then await each one inside its own try/catch so you can keep the successes and record the failures.

solid answer

~40 s

Use supervisorScope because the calls are independent — a failure in one must not cancel siblings. Start each call with async, then await each Deferred individually inside a try/catch (or wrap each await in runCatching) to turn it into a success or failure value. If you used coroutineScope instead, the first failure would cancel the remaining calls and rethrow, giving you nothing. Important nuance: with supervisorScope an async child's exception is only observed when you call await(); if you never await a failed child its exception is effectively swallowed. Keep CancellationException distinct — never swallow it, since it signals legitimate cancellation, not a call failure.

code

kotlin · 7 lines
kotlin
suspend fun fetchAll(urls: List<String>) = supervisorScope {
    urls.map { url -> async { httpClient.get(url) } }
        .map { d ->
            runCatching { d.await() }
                .onFailure { if (it is CancellationException) throw it }
        }
}

go deeper

for a junior

Picks supervisorScope and knows it keeps other calls running on failure.

for a middle

Writes the async-then-await-with-try/catch pattern and explains why coroutineScope is wrong here.

for a senior

Adds the unobserved-async-failure nuance and CancellationException handling, and reasons about bounding concurrency.

for a principal

Designs the contract (Result list vs throwing), backpressure/concurrency limits, timeouts per call, and how failures surface to callers and metrics.

## The requirement 'Partial results even if some fail' means the tasks are **independent**: one failing call should not cancel the others. That maps directly to `supervisorScope`, whose `SupervisorJob` does not propagate child failures upward to siblings. ## The pattern ```kotlin suspend fun fetchAll(urls: List<String>): List<Result<Response>> = supervisorScope { val deferreds = urls.map { url -> async { httpClient.get(url) } // independent child } deferreds.map { d -> runCatching { d.await() } // success or failure per task .onFailure { if (it is CancellationException) throw it } } } ``` Key points: - **`supervisorScope`** keeps siblings alive when one `async` fails. - **`async` + per-item `await()`** lets each call resolve independently. A failed `async` **stores** its exception and rethrows it at `await()` — which is why each `await()` is wrapped. - **`runCatching`/`try-catch` per await** converts failures into values so you keep the successes. ## Why not coroutineScope With `coroutineScope`, the first failing child cancels the siblings and the whole scope rethrows. You'd lose all partial results — the opposite of the requirement. ## Two important nuances 1. **Unobserved async failures.** Under a SupervisorJob, an `async` child's exception surfaces **only** at `await()`. If you forget to await a failed child, its exception is silently dropped. So always await every Deferred you launch (the pattern above does). 2. **Don't swallow `CancellationException`.** A blanket `catch (e: Exception)` or `runCatching` will also catch `CancellationException`, which signals that the coroutine itself was cancelled (e.g. caller gave up). Swallowing it breaks cooperative cancellation. Rethrow it explicitly, as shown. ## Bounding concurrency If N is large you typically bound concurrency with a `Semaphore` or `limitedParallelism` dispatcher inside the same supervisorScope, so a fan-out of hundreds of calls doesn't overwhelm the system — but the scope builder choice stays the same.

  • Why must you await every async child here, even the ones that might fail?
    Under a SupervisorJob an async exception is stored and only rethrown at await(). If you never await a failed child, its failure is silently swallowed and you can't record it.
  • Why rethrow CancellationException instead of catching it as a failure?
    CancellationException signals the coroutine was cancelled, not that the network call failed. Swallowing it breaks cooperative cancellation and can hang structured concurrency.

saying these in an interview costs you the question

  • Choosing coroutineScope and losing all results on first failure
  • Catching CancellationException as if it were a call error
  • Forgetting that async failures surface only at await()
  • Awaiting all in one try/catch so the first failure aborts the rest
  • Ignoring concurrency bounding for very large N

context