Structured concurrency

Coroutines Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Predict the output

The child that loads reviews throws. What happens to the other child and to loadPage?

suspend fun loadPage(): Page = coroutineScope {
    val details = async { api.details() }   // 2 s
    val reviews = async { api.reviews() }   // throws after 0.5 s
    Page(details.await(), reviews.await())
}
Options
Hint 1 of 2

What does a failing child do to its parent?

Hint 2 of 2

What does the parent then do to its other children?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A details is cancelled and loadPage throws the reviews error

Correct. A failing child cancels its siblings, and coroutineScope rethrows the failure.

Why not the others?
  • B details keeps running and loadPage returns a Page without reviews. That would need supervisorScope plus try/catch around reviews.await().
  • C loadPage waits for details, then throws. The sibling is cancelled at once, not awaited to completion.
  • D The error is ignored until someone calls await on reviews. In coroutineScope, the failure cancels the scope straight away.
Indented job tree: loadPage scope with details and reviews children. Reviews fails; compare coroutineScope with supervisorScope.
Text version

With coroutineScope (reviews fails)

  1. reviews throws
  2. The scope is cancelled
  3. details is cancelled
  4. loadPage rethrows the error
  • loadPage scope: ✕ failed
  • details: ⊘ cancelled
  • reviews: ✕ failed

With supervisorScope (reviews fails)

  1. reviews throws
  2. The supervisor ignores it for the other child
  3. details completes
  4. reviews.await() throws where you call it
  • loadPage scope: ● active
  • details: ✓ completed
  • reviews: ✕ failed

30-second answer

Structured concurrency means every coroutine has a parent scope, and a scope does not finish until all its children finish. coroutineScope { } suspends until every child completes, cancels the rest if one fails, and rethrows the failure. Work can't leak past the function that started it.

Practise saying it · 45 s. No recording, just a timer

Explain structured concurrency and what coroutineScope guarantees.

Say your answer out loud, then check which of these points you covered:

  • Every coroutine has a parent
  • The scope waits for all children
  • A failure cancels siblings and is rethrown
  • supervisorScope keeps siblings running

Deep dive

Cancelling a parent cancels all its children. A failing child cancels its parent and therefore its siblings.

coroutineScope is for parallel work inside a suspend function. supervisorScope is the same except one child's failure doesn't cancel the others.

Because the scope waits, a suspend function that returns has no work still running in the background.

Breaking the tree with GlobalScope or a new Job() passed to launch loses cancellation and error propagation.

Page model used above Compile-checked · Kotlin 2.4.21
data class Page(val details: String, val reviews: List<String>)

interface PageApi {
    suspend fun details(): String
    suspend fun reviews(): List<String>
}

Common mistakes

  • Passing Job() into launch(Job()), which detaches the child from its parent.
  • Using supervisorScope everywhere and silently losing failures.
  • Thinking a function using coroutineScope returns before its children finish.

Senior take & follow-ups

Draw the tree. Interviewers like candidates who can say exactly which jobs end up cancelled when one child throws.

Follow-up questions

Think of your answer first, then open the hint.

  1. What does launch(Job()) { } do to the tree?

    Show hint

    It makes the new Job the parent, so cancelling the outer scope no longer cancels it.

  2. When would you choose supervisorScope?

    Show hint

    When children are independent, such as loading separate widgets on a dashboard.

Compare on iOS: async let vs TaskGroup Swift task groups and Kotlin coroutineScope give the same structured guarantee.

Suggest a fix if something here is wrong or out of date.