When would you use supervisorScope { } versus constructing a CoroutineScope with SupervisorJob(), and how do they differ in lifecycle?
answer
- supervisorScope = bounded, suspends until children done
- SupervisorJob() scope = long-lived, you cancel() it
- Block-body exception propagates; launched-child exception goes to handler
- viewModelScope/lifecycleScope are SupervisorJob-backed
- async stores exception until await()
basics
~20 sUse supervisorScope { } for a temporary block of parallel tasks that should not cancel each other. Use a CoroutineScope built with SupervisorJob() for a long-lived owner, like a screen or service, that you cancel later.
solid answer
~50 ssupervisorScope { } is a suspending builder: it creates a child scope backed by a supervisor Job, runs the block, and suspends until all children complete before returning — like coroutineScope but without sibling cancellation on failure. It is scoped to that block and rethrows any exception thrown directly in the block body. CoroutineScope(SupervisorJob() + dispatcher) creates a standalone, long-lived scope you store in a property (e.g. ViewModel, service); you launch into it over time and explicitly call scope.cancel() in cleanup. Lifecycle difference: supervisorScope auto-joins/auto-completes within the call; a manually built scope lives until you cancel it. Both isolate child failures from siblings. A subtle trap: in supervisorScope, an exception in a directly-launched child is NOT rethrown by the block — it is delivered to the child's handler — whereas an exception thrown in the block's own code IS propagated out.
code
kotlin · 11 linesclass Repo {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun start() = scope.launch { /* survives sibling failures */ }
fun close() = scope.cancel()
}
suspend fun aggregate() = supervisorScope {
val x = async { fetchX() }
val y = async { fetchY() }
runCatching { x.await() }.getOrNull() to y.await()
}go deeper
Can pick supervisorScope for a temporary parallel block and a SupervisorJob scope for something long-lived.
Explains the lifecycle difference (auto-join vs manual cancel) and that both isolate siblings.
Articulates the propagation subtlety between block-body and launched-child exceptions and ties it to viewModelScope.
Designs scope ownership and teardown across a system, avoiding leaks and choosing isolation boundaries deliberately.
## Two tools, same isolation policy Both give you **supervisor semantics** (a failing child does not cancel its siblings), but they differ in **lifecycle and ergonomics**. ## supervisorScope { } — a bounded, suspending builder `supervisorScope { }` is a **suspending function** that: 1. Creates a new child scope whose Job is a supervisor. 2. Runs the lambda you pass. 3. **Suspends until every coroutine launched inside completes** (success or failure), then returns. It mirrors `coroutineScope { }`, except failures of children launched inside are isolated. Use it inside a suspend function when you fan out several independent sub-tasks and want partial results to survive. ```kotlin suspend fun loadDashboard(): Dashboard = supervisorScope { val a = async { loadFeed() } // may fail val b = async { loadProfile() } // independent Dashboard( feed = runCatching { a.await() }.getOrNull(), profile = b.await() ) } ``` ### Propagation subtlety inside supervisorScope - An exception thrown **directly in the block body** (e.g. `b.await()` rethrowing) **propagates out** of `supervisorScope` to the caller. - An exception in a **directly-launched child** (`launch { throw ... }`) is treated as that child's failure and is delivered to its `CoroutineExceptionHandler`, **not** rethrown by the block. - For `async`, the exception is stored in the `Deferred` and only thrown when you call `await()`. ## CoroutineScope(SupervisorJob()) — a long-lived owner When a component lives across many operations (a UI screen, a background service), you build a scope you control: ```kotlin class FeedService(dispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(SupervisorJob() + dispatcher) fun refresh() = scope.launch { /* one independent task */ } fun shutdown() = scope.cancel() // cancels all children downward } ``` Here you `launch` into the scope over time, and you are responsible for calling `cancel()` during teardown. Android's `viewModelScope` and `lifecycleScope` are built exactly this way (on a `SupervisorJob`). ## Choosing between them | Situation | Use | |---|---| | Fan-out inside a single suspend function, want to join | `supervisorScope { }` | | Long-lived owner that launches work over time | `CoroutineScope(SupervisorJob() + ...)` | ## Key APIs - `supervisorScope { }`, `coroutineScope { }` - `SupervisorJob()`, `CoroutineScope(...)`, `scope.cancel()` - `async` / `await`, `runCatching`, `CoroutineExceptionHandler`
- In supervisorScope, does an exception thrown by launch { } propagate to the caller of supervisorScope?No. A directly-launched child's failure is isolated and routed to its CoroutineExceptionHandler; only exceptions thrown in the block body itself propagate out.
- Why is viewModelScope backed by a SupervisorJob?So that one failing coroutine on a screen does not cancel all the other in-flight work on that same screen.
supervisorScope is a rented meeting room you must vacate when the meeting ends; a SupervisorJob scope is an office you keep until you decide to move out.
saying these in an interview costs you the question
- Saying both are identical with no lifecycle distinction
- Claiming supervisorScope returns immediately without waiting for children
- Forgetting to cancel() a manually built long-lived scope (leak)
- Thinking a launch failure inside supervisorScope is rethrown to the caller