skip to content

Explain precisely when a coroutineScope { } block returns. If the block's last statement runs but a launched child is still working, what happens?

level: middleimportance: should knowfreq 55%

answer

  1. Returns after last child, not last line
  2. Job 'Completing' state = body done, children alive
  3. Closing brace suspends until children finish
  4. Child failure -> cancel siblings -> rethrow
  5. No way to return with live spawned work

basics

~10 s

The block returns only after every coroutine started inside it has finished. Even if the last line ran, if a child is still working the block keeps waiting for it.

solid answer

~50 s

coroutineScope { } runs the block body, but its return is gated on completion of all children, not just the body. When the body finishes, the scope's Job enters a 'completing' state: it has produced its result but waits for every child Job to complete. Only when the last child finishes does the scope actually complete and coroutineScope return. So launch { } a long task and then reaching the closing brace does NOT end the scope — control suspends at the brace until that child is done. If any child fails during this window, the scope cancels the remaining children and coroutineScope rethrows. This is the mechanism that prevents leaks: there is no way for the function to return while spawned work is still alive. The same applies to supervisorScope and to the scope behind runBlocking and the body of an actor — children are always awaited.

go deeper

for a junior

Knows the block waits for children before returning, even without naming the Completing state.

for a middle

Names the Completing state and explains body-done vs children-alive, plus failure rethrow.

for a senior

Explains why this is the no-leak invariant, ties it to suspension (not blocking), and notes the same rule across supervisorScope/runBlocking.

for a principal

Reasons about implications for timeouts, structured shutdown, and how the invariant guarantees resource cleanup ordering.

## The 'completing' state A `Job` has lifecycle states. The key one here is **Completing**: the coroutine's own code has finished and produced a result, *but it still has active children*. A `Job` only reaches **Completed** once it has no active children left. `coroutineScope { }` returns when its underlying `Job` reaches **Completed** — i.e. *after the last child finishes* — not when the block's last line executes. ```kotlin import kotlinx.coroutines.* suspend fun demo() = coroutineScope { launch { delay(1000) println("child finished") } println("reached closing brace") // prints first } // <-- coroutineScope SUSPENDS here until the child prints, THEN returns // Output order: // reached closing brace // child finished ``` ## Why this is the heart of structured concurrency The whole no-leak guarantee depends on this: **a parent cannot complete while a child is alive.** If `coroutineScope` returned at the closing brace, the child would become an orphan running after the function ended — exactly the leak structured concurrency forbids. ## What if a child fails during the wait? If a `launch`ed child throws while the scope is in *completing*: 1. the scope is cancelled, 2. its other children are cancelled, 3. `coroutineScope` rethrows the exception to the caller. ```kotlin suspend fun failFast() = coroutineScope { launch { delay(50); error("boom") } // fails launch { delay(10_000) } // gets cancelled } // coroutineScope throws IllegalStateException("boom"), other child cancelled ``` ## Same behavior elsewhere The await-all-children rule also governs `supervisorScope { }` (children awaited, but a child failure isn't propagated to siblings), the `runBlocking { }` body, and `withContext { }` blocks that launch children. Anywhere a scope ends, it waits for its children. ## Practical takeaway If a function looks like it returns too early but `delay`/IO inside a child still runs, that's the suspension at the closing brace doing its job. To *not* wait, you'd have to break structure (own an outer scope) — which is the unstructured path.

  • What Job state describes 'body done but children still running'?
    The Completing state — the Job has its result but waits for active children before reaching Completed.
  • Does this waiting block the thread?
    No — coroutineScope suspends, freeing the thread; it resumes when the children complete.

A manager who has finished their own report but can't clock out until every team member they assigned a task to is also done.

saying these in an interview costs you the question

  • Saying coroutineScope returns when the last statement runs
  • Believing a still-running launched child can outlive the block
  • Confusing thread-blocking with coroutine suspension at the brace
  • Not knowing about the Completing state
  • Thinking you must manually join every child (the scope does it)

context