What is the difference between coroutineScope and supervisorScope when one of their child coroutines throws an exception?
answer
- coroutineScope = all-or-nothing; one fails, all cancelled + rethrow
- supervisorScope = isolate; siblings survive, handle locally
- Regular Job vs SupervisorJob is the difference
- async defers exception to await()
- CancellationException never cancels siblings
basics
~10 sIn coroutineScope, if one child fails, all the other children are stopped and the failure is rethrown. In supervisorScope, a failing child does not stop its siblings; each child fails independently.
solid answer
~40 sBoth are suspending scope builders that wait for all children. The difference is failure handling. coroutineScope creates a regular Job: when any child throws a non-CancellationException, the scope cancels all sibling children and rethrows that exception from the coroutineScope { ... } call. supervisorScope creates a SupervisorJob: a child failure is NOT propagated upward, so siblings keep running. With supervisorScope you handle each child's failure locally — typically a try/catch around the body of each launch, or a CoroutineExceptionHandler for top-level coroutines started via launch. async children in either scope defer their exception until await(). supervisorScope itself still rethrows only if its own body throws. Both join all children before returning, so structured concurrency (no leaked coroutines) holds in both cases.
code
kotlin · 22 linesimport kotlinx.coroutines.*
suspend fun main() {
// coroutineScope: failure cancels the sibling
try {
coroutineScope {
launch { delay(50); println("sibling done") } // gets cancelled
launch { throw RuntimeException("fail A") }
}
} catch (e: Exception) {
println("caught: ${e.message}") // caught: fail A
}
// supervisorScope: sibling survives
supervisorScope {
launch {
try { throw RuntimeException("fail B") }
catch (e: Exception) { println("handled: ${e.message}") }
}
launch { delay(50); println("sibling survived") }
}
}go deeper
Knows the headline: coroutineScope cancels siblings on failure, supervisorScope isolates them.
Explains the regular-Job vs SupervisorJob mechanism and that coroutineScope rethrows while supervisorScope requires local handling.
Adds launch-vs-async semantics, CancellationException special-casing, and when CoroutineExceptionHandler actually fires.
Reasons about when to choose each in a real system (independent vs dependent subtasks), and the failure-isolation/observability trade-offs.
## The two scope builders Both `coroutineScope` and `supervisorScope` are **suspending functions** that create a new coroutine scope, run the supplied block, and **suspend until all child coroutines launched inside complete**. They are the building blocks of *structured concurrency*: no coroutine started inside escapes the call. The difference is entirely in **how a child failure propagates**. ## coroutineScope — failure cancels everyone `coroutineScope` installs a regular `Job` as the parent of the children launched inside it. The propagation rule for a regular Job is: - When a child throws an uncaught exception (anything that is **not** a `CancellationException`), the failure travels **up** to the parent Job. - The parent then **cancels all of its other children** (siblings of the failing one). - The original exception is **rethrown** from the `coroutineScope { ... }` expression, so the caller can `try/catch` it in one place. ```kotlin suspend fun load() { coroutineScope { launch { throw IllegalStateException("boom") } // fails launch { delay(10_000); println("never prints") } // cancelled } } // the IllegalStateException is rethrown from coroutineScope ``` ## supervisorScope — failures are isolated `supervisorScope` installs a `SupervisorJob` as the parent. A `SupervisorJob` **does not propagate child failures upward**: - A failing child does **not** cancel its siblings. - The failure is **not** rethrown from `supervisorScope` (the scope only rethrows if **its own body** throws directly). - You must handle each child's failure where it is started. ```kotlin supervisorScope { launch { try { riskyWork() } catch (e: Exception) { recover(e) } } launch { otherWork() } // keeps running even if the first fails } ``` ## launch vs async inside these scopes - `launch` exposes failures **eagerly**: an uncaught exception propagates immediately per the rules above. - `async` **defers** the exception: it is stored in the resulting `Deferred` and re-thrown only when you call `await()`. In a `coroutineScope`, an `async` child that fails still cancels the scope (the parent Job sees the failure), but you observe the exception at `await()`. In a `supervisorScope`, a failing `async` is isolated — you must `try/catch` the `await()`. ## CancellationException is special Cancellation is **cooperative and structured**: throwing `CancellationException` cancels only the current coroutine and is treated as normal completion by the parent — it does **not** cancel siblings and is not reported as a failure. This is why catching a broad `Exception` and swallowing it can break cancellation; rethrow `CancellationException`. ## CoroutineExceptionHandler A `CoroutineExceptionHandler` is only invoked for **uncaught** exceptions in **root** coroutines launched with `launch` (it is ignored on `async` and on child coroutines whose parent handles the failure). It is commonly paired with `supervisorScope`/`SupervisorJob` to log uncaught child failures. ## Summary table - coroutineScope → regular Job → child failure cancels siblings + rethrows. - supervisorScope → SupervisorJob → child failure isolated, handle locally. - Both → wait for all children (structured), both honor CancellationException semantics.
- If you use supervisorScope but launch a child that throws and you do NOT catch it, what happens?The failure is not rethrown by supervisorScope and siblings keep running, but the exception is still 'uncaught' — it goes to the CoroutineExceptionHandler if one is installed, otherwise to the default handler (e.g. prints to stderr / crashes the thread depending on platform).
- Does a failing async child cancel a coroutineScope even before you call await()?Yes. async still attaches to the parent Job, so the failure propagates to the scope and cancels siblings; await() is where you actually observe/rethrow the stored exception.
coroutineScope is a team where one person's failure aborts the whole mission; supervisorScope is a supervisor who lets each worker fail without stopping the others.
saying these in an interview costs you the question
- Saying supervisorScope catches exceptions for you (it doesn't — you still must handle them)
- Claiming a failing async in coroutineScope is harmless until await()
- Treating CancellationException as a normal failure that cancels siblings
- Believing supervisorScope rethrows child failures from its body
- Thinking CoroutineExceptionHandler works on async or on non-root children