skip to content

How does supervisorScope { } differ from coroutineScope { } with respect to child failures?

level: middleimportance: must knowfreq 65%

answer

  1. Both wait for all children, both cancel downward
  2. coroutineScope: any failure cancels all + rethrows from builder
  3. supervisorScope: failure isolated, NOT rethrown by builder
  4. try/catch around supervisorScope won't catch child errors
  5. async failure surfaces on await() in both

basics

~10 s

Both wait for all child coroutines to finish. In coroutineScope, one child failing cancels all the others and throws. In supervisorScope, a failing child does not cancel its siblings.

solid answer

~40 s

Both `coroutineScope { }` and `supervisorScope { }` are suspending builders that create a child scope, run the block, and suspend until every child coroutine completes. The difference is the backing Job. `coroutineScope` uses a regular Job: if any child fails, the scope cancels all remaining children and rethrows the exception from the builder call. `supervisorScope` uses a SupervisorJob: a child's failure does not cancel siblings and is NOT rethrown by the builder — instead it propagates to that child's own CoroutineExceptionHandler (for launch) or surfaces on await() (for async). Both still propagate **downward** cancellation, and both wait for children before returning. Use `coroutineScope` for all-or-nothing work (decompose one task into parallel parts); use `supervisorScope` when children are independent and one failure should not abort the rest.

go deeper

for a junior

Knows that coroutineScope cancels all children on failure while supervisorScope isolates them.

for a middle

Explains that supervisorScope does NOT rethrow from the builder and that exceptions must be handled per-child.

for a senior

Distinguishes propagation paths (CoroutineExceptionHandler vs await), and that both wait for children and cancel downward.

for a principal

Advises on choosing all-or-nothing vs independent semantics per use case and the testability/error-reporting implications.

## The two builders Both are **suspending functions** that establish structured concurrency for a block of code: - They create a new child scope whose Job is a child of the current coroutine's Job. - They run the lambda (which can `launch`/`async` children). - They **suspend the caller until all children complete**, then return. The key difference is the **type of Job** they install. ## coroutineScope { } — regular Job, all-or-nothing Backed by a normal `Job`. If **any** child fails: 1. All other children are cancelled. 2. The exception is **rethrown from the `coroutineScope(...)` call** itself, so you can wrap it in `try/catch`. ```kotlin try { coroutineScope { launch { delay(100); println("never prints") } launch { throw RuntimeException("boom") } } } catch (e: RuntimeException) { println("caught: ${e.message}") // caught: boom } ``` ## supervisorScope { } — SupervisorJob, independent children Backed by a `SupervisorJob`. A child's failure: - Does **not** cancel siblings. - Is **not** rethrown by the builder — wrapping `supervisorScope { }` in try/catch will not catch a child's exception. - For `launch` children, it goes to the nearest `CoroutineExceptionHandler` in that child's context (or the uncaught handler). - For `async` children, it is stored and rethrown only when you call `await()`. ```kotlin supervisorScope { launch(CoroutineExceptionHandler { _, e -> println("handled: ${e.message}") }) { throw RuntimeException("boom") } launch { delay(200); println("sibling survives") } } ``` ## Shared behavior - Both **wait for all children** (the scope does not return early). - Both propagate **downward cancellation**: cancelling the outer coroutine cancels the whole scope. - A direct exception thrown by the block body itself (not a child) cancels the scope in both cases. ## Choosing between them - **coroutineScope**: parallel decomposition of one logical task — if any part fails the whole thing is meaningless. - **supervisorScope**: a batch of independent tasks (e.g. fan-out requests) where partial failure is acceptable.

  • Can you catch a child's exception by wrapping supervisorScope in try/catch?
    No. supervisorScope does not rethrow child failures from the builder. Catch it inside the child (try/catch in the launch block), via a CoroutineExceptionHandler on the child, or on await() for async.
  • If the supervisorScope block body itself throws (outside any launch), what happens?
    That cancels the scope and propagates like a normal exception from the builder — supervisor behavior only isolates child coroutines, not the body.

saying these in an interview costs you the question

  • Saying supervisorScope rethrows child exceptions from the builder
  • Believing supervisorScope returns before children finish
  • Claiming coroutineScope keeps siblings alive on failure
  • Forgetting both still cancel downward
  • Suggesting try/catch around supervisorScope catches child errors

context