skip to content

coroutineScope Cancellation Semantics

Inside coroutineScope a failure cancels the sibling children and rethrows to the caller; supervisorScope isolates the failure instead. Knowing that CancellationException is treated as normal completion, not an error, is the piece that makes the rest coherent.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What does the coroutineScope { } builder do, and how is it different from launching with GlobalScope?

level: juniorimportance: must knowfreq 65%

answer

  1. coroutineScope suspends until all children finish
  2. Inherits caller context, adds a new parent Job
  3. Child failure cancels siblings + rethrows
  4. GlobalScope = app-lifetime, no owner, leak risk
  5. Prefer coroutineScope or a lifecycle-bound scope

basics

~20 s

coroutineScope runs a block, starts child coroutines inside it, and waits until all of them finish before returning. GlobalScope launches coroutines that are not tied to anything and can outlive your code, so they can leak.

solid answer

~40 s

`coroutineScope` is a suspending function that creates a child scope, runs its block, and **suspends until every coroutine started inside it completes**. It inherits the caller's `CoroutineContext` (dispatcher, etc.) and adds a new Job as parent. This gives structured concurrency: when `coroutineScope` returns, no work is still running, and if a child fails the scope cancels the rest and rethrows. `GlobalScope`, by contrast, is a rootless scope tied to the whole application lifetime: coroutines launched there have no parent to wait for them or cancel them, so they can outlive the surrounding function and leak. Idiomatic Kotlin avoids `GlobalScope` (it is marked `@DelicateApi`) and instead uses `coroutineScope`, a lifecycle-bound scope (e.g. `viewModelScope`), or passes a `CoroutineScope`.

go deeper

for a junior

Knows coroutineScope waits for children and that GlobalScope can leak — the core takeaway.

for a middle

Explains context inheritance, the new parent Job, and the suspend (non-blocking) nature.

for a senior

Contrasts with withContext and lifecycle scopes, and articulates failure propagation precisely.

for a principal

Sets team conventions banning GlobalScope and prescribing owned/lifecycle scopes across the codebase.

## coroutineScope in one sentence `coroutineScope { ... }` is a **suspending function** that creates a new scope bound to the calling coroutine, runs the block, and **does not return until all children launched inside have finished**. ## What it gives you - **Inherits context**: it uses the caller's `CoroutineContext` (dispatcher, name) and installs a fresh `Job` as the parent of inner coroutines. - **Joins children**: `launch`/`async` started inside are awaited automatically — you don't manage join() by hand. - **Structured failure**: if a child throws, siblings are cancelled and the exception is rethrown from the `coroutineScope` call. ```kotlin suspend fun fetchAll(): List<String> = coroutineScope { val a = async { fetchA() } val b = async { fetchB() } listOf(a.await(), b.await()) // both done before we return } ``` ## GlobalScope — the rootless escape hatch `GlobalScope` is a built-in `CoroutineScope` whose Job lives for the **entire application**. Coroutines launched there: - have **no structural parent** to wait on or cancel them, - can **outlive** the function/component that started them, - are easy to **leak** (keep running, holding resources, after the screen/request is gone). It is annotated `@DelicateApi` precisely because misuse breaks structured concurrency. ```kotlin // Anti-pattern: nobody owns or cancels this GlobalScope.launch { doForever() } ``` ## What to use instead - `coroutineScope { }` inside a `suspend` function for fan-out/fan-in. - A lifecycle-bound scope: `viewModelScope`, `lifecycleScope`, or a custom `CoroutineScope(SupervisorJob() + Dispatchers.Default)` you cancel on teardown. - Pass a `CoroutineScope` to the function so the caller owns the lifetime. ## Related builder: withContext vs coroutineScope `withContext(ctx) { }` also runs a block and waits, but its purpose is to **switch context/dispatcher**. `coroutineScope` keeps the same context and exists to **group children for structured concurrency**. ## Summary - coroutineScope = scoped, waits for children, propagates failure, inherits context. - GlobalScope = app-lifetime, no parent, leak risk, delicate API — avoid.

  • Is coroutineScope blocking or suspending?
    Suspending — it must be called from a coroutine or another suspend function and it suspends (does not block the thread) until children complete.
  • Why is GlobalScope marked @DelicateApi?
    Because launching there detaches work from any lifecycle, so it can leak and break structured concurrency; you must justify and manage it carefully.

coroutineScope is a meeting room you can't leave until everyone is done; GlobalScope is sending people off into the world with no one tracking when they come back.

saying these in an interview costs you the question

  • Saying coroutineScope blocks the thread
  • Claiming GlobalScope coroutines are automatically cancelled with the caller
  • Confusing coroutineScope with runBlocking
  • Thinking coroutineScope changes the dispatcher (that's withContext)
  • Recommending GlobalScope for normal app work

context

open as a page

What is the difference between coroutineScope and supervisorScope when one of their child coroutines throws an exception?

level: middleimportance: must knowfreq 78%

basics

~10 s

In 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.

open as a page

Why is CancellationException treated specially during structured exception propagation, and what bug does swallowing it cause?

level: seniorimportance: must knowfreq 70%

basics

~20 s

Cancellation works by throwing a special CancellationException. It only stops the coroutine being cancelled, not its siblings, and the parent treats it as normal. If your catch block swallows it, the coroutine ignores cancellation and keeps running.

open as a page

Inside coroutineScope you start two async tasks; one throws before you call await(). Does the scope fail, and when do you observe the exception?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Yes — because async is still a child of the scope, the failure cancels the scope and its siblings right away. But the actual exception value is re-thrown when you call await() on that task.

open as a page

You run multiple independent background subtasks and one failing must not kill the others. How do you structure the scope, and what are the failure-observability trade-offs?

level: principalimportance: should knowfreq 42%

basics

~10 s

Use supervisorScope (or a scope built on SupervisorJob) so one task's failure doesn't cancel the others. The trade-off: each task must handle or report its own errors, or failures get lost.

open as a page