Contrast launching work with coroutineScope { } versus GlobalScope.launch { }. Which respects structured concurrency, and what goes wrong with the other?
answer
- coroutineScope = bound to caller, waits, propagates
- GlobalScope = bound to process, no parent, leaks
- GlobalScope is @DelicateApi
- Own a real scope or make it suspend
- Structured keeps errors where caller sees them
basics
~10 scoroutineScope ties work to the current call and waits for it to finish. GlobalScope ties work to the whole app, so nobody waits for it or cancels it, and it can leak.
solid answer
~50 scoroutineScope { } creates a child scope bound to the caller: it suspends until every coroutine launched inside finishes, propagates cancellation in and failures out, and respects the caller's Job hierarchy. That is structured concurrency. GlobalScope.launch { } creates a coroutine whose Job has no parent — its lifetime is the whole process. The caller returns immediately while the work keeps running; nobody awaits it, cancelling the caller's scope does not cancel it, and its exceptions surface only through the uncaught-exception handler. So you get leaks, runaway work after the caller is gone, and tasks that outlive the unit test or request that started them. GlobalScope is annotated @DelicateApi precisely for this reason. The structured alternative is to accept or own a CoroutineScope (e.g. a viewModelScope, an application-lifecycle scope, or a coroutineScope { } block) so every coroutine has a real owner.
code
kotlin · 13 linesimport kotlinx.coroutines.*
// Structured: caller is cancelled -> work is cancelled, errors rethrow
suspend fun fetchAll() = coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
a.await() + b.await()
}
// Unstructured: orphaned, survives the caller, errors hidden
fun fetchAllLeaky() {
GlobalScope.launch { fetchA() } // nobody waits or cancels
}go deeper
Knows coroutineScope waits and GlobalScope leaks, even if vague on Job parentage.
Explains parent/no-parent Job, error rethrow vs global handler, and the @DelicateApi marking.
Prescribes the structured alternative (own a lifecycle scope / make it suspend) and reasons about cancellation propagation and testability.
Connects the choice to API contracts, resource safety under load, and how leaks degrade a service over time; sets team conventions banning GlobalScope.
## Two ways to start coroutines ### coroutineScope { } — structured `coroutineScope` is a **suspending function**. It creates a new scope whose `Job` is a **child of the current coroutine's Job**, runs your block, and **does not return until all coroutines started inside have finished**. ```kotlin suspend fun processBatch(ids: List<Int>): List<Result> = coroutineScope { ids.map { id -> async { process(id) } } // children of this scope .awaitAll() // scope waits for all } ``` Guarantees you get for free: - **Bound lifetime** — when the caller is cancelled, so is everything inside. - **Wait-for-children** — control returns only when all work is done. - **Failure propagation** — if one `async` throws, the scope cancels the siblings and rethrows. ### GlobalScope.launch { } — unstructured `GlobalScope` is a scope tied to the **entire application lifetime**. Its `Job` has **no parent**. ```kotlin fun handleRequest() { GlobalScope.launch { doExpensiveWork() } // fire-and-forget, orphaned // handleRequest returns; the coroutine keeps running with no owner } ``` Problems: - **Leaks** — nobody holds or awaits the `Job`; it runs until it finishes or the process dies. - **No cancellation propagation** — cancelling the caller's scope does nothing to it. - **Hidden errors** — failures don't propagate to a parent; they hit the global `CoroutineExceptionHandler` (or crash). - **Test flakiness** — work outlives the test that started it. That's why `GlobalScope` carries `@DelicateApi` and the docs steer you away from it. ## The rule of thumb Don't invent a coroutine out of thin air. Either: 1. make the function `suspend` and use `coroutineScope { }` / `supervisorScope { }`, or 2. take/own a real `CoroutineScope` whose lifecycle matches a real thing (a screen, a request, the app). ## Subtle point `coroutineScope` rethrows the failure of any child after cancelling the rest, so the caller sees the error. `GlobalScope.launch` swallows it into the global handler. Structured concurrency keeps errors and lifetimes *where the caller can reason about them*.
- If you truly need fire-and-forget background work, what's the structured way?Launch it on a long-lived, explicitly-owned CoroutineScope (e.g. an application-lifecycle scope with a SupervisorJob) that something is responsible for cancelling — not GlobalScope.
- How does coroutineScope surface a child's exception?It cancels the remaining children and rethrows the exception from the coroutineScope call, so the caller can catch it.
saying these in an interview costs you the question
- Claiming GlobalScope and coroutineScope are interchangeable
- Saying GlobalScope.launch's errors propagate to the caller (they don't)
- Not knowing coroutineScope suspends until children finish
- Recommending GlobalScope for ordinary request/screen-scoped work
- Thinking 'structured' just means 'looks nicer' rather than lifetime ownership