Why does a parent coroutine not complete until its children finish, and what is the 'Completing' state?
answer
- Parent body done != parent done
- Completing = body finished, awaiting children
- Completed only after last child terminal
- join() returns after the whole subtree
- Child failure during Completing still propagates up
basics
~20 sA parent waits for all its children to finish before it counts as done. Even after the parent's own code runs out, it stays in a waiting phase until every child it started has completed.
solid answer
~40 sWhen a parent coroutine's own body finishes, it does not immediately complete — it enters the Completing state, where isActive is still effectively true and it waits for all child Jobs to reach a terminal state. Only when the last child completes does the parent transition to Completed. This is a core structured-concurrency guarantee: launching child coroutines without explicitly joining them is safe, because the parent (or the enclosing coroutineScope) implicitly joins them. If any child fails during this window, the failure propagates up and cancels the parent and its siblings (unless a SupervisorJob is in play). join() on the parent therefore only returns once the whole subtree is done.
code
kotlin · 12 linesimport kotlinx.coroutines.*
fun main() = runBlocking {
val parent = launch {
launch { delay(300); println("child") }
println("parent body")
// parent now in Completing, awaiting the child
}
parent.join()
println("parent completed")
}
// Order: parent body -> child -> parent completedgo deeper
Understands a parent waits for the children it launched before being done.
Explains the Completing state and the body-done vs job-done distinction, and that join waits for the whole subtree.
Connects the guarantee to coroutineScope determinism and to failure propagation during the Completing window.
Frames it as the structural invariant 'parent outlives children' that makes structured concurrency leak-free, and the trade-offs of opting out.
## The guarantee In structured concurrency, **a parent outlives all of its children**. Concretely: a parent `Job` cannot reach the **Completed** state until **every** child `Job` has reached a terminal state (Completed or Cancelled). ## The Completing state When the parent's *own* code (the lambda body) returns but it still has running children, it transitions into the **Completing** state: - The body has finished, so no more of the parent's own code runs. - But the Job is not yet terminal; it is **waiting for children**. - From the outside, the parent still appears unfinished — `isCompleted` is `false`, and `join()`/`await()` keep suspending. Once the last child completes, the parent finally moves Completing -> Completed (or, if a child failed, Completing -> Cancelling -> Cancelled). ```kotlin val parent = scope.launch { launch { delay(500); println("child done") } println("parent body done") // prints first // <-- parent is now Completing, still waiting for the child } parent.join() // returns only after "child done" println("parent completed") // prints last ``` Output order: `parent body done`, `child done`, `parent completed`. ## Why this matters - **No fire-and-forget by accident.** You can write `launch { ... }` inside a parent without explicitly tracking the returned Job; the parent (or `coroutineScope`) will still wait for it. No silently-leaked background work. - **Deterministic shutdown.** When `coroutineScope { }` returns, every coroutine started inside is guaranteed finished. - **Failure window.** If a child fails while the parent is Completing, that failure still propagates upward and turns the parent's completion into a cancellation (unless interrupted by a `SupervisorJob`). ## Common confusion People expect `launch` inside a coroutine to be "fire and forget". It is not detached: the surrounding Job tracks it. To truly detach, you must give it a different parent (e.g. `launch(Job())` or a different long-lived scope) — which then loses these guarantees.
- Can you observe the Completing state via isActive?Practically, isActive stays true during Completing (the job is not yet terminal and not cancelled), while isCompleted is false. There is no dedicated boolean for Completing itself.
- What if a child fails while the parent is Completing?The child's failure propagates to the parent, cancelling remaining children and turning the parent's completion into a cancellation — unless a SupervisorJob breaks the upward propagation.
A party host doesn't leave their own party until the last guest has left, even after they've stopped serving food.
saying these in an interview costs you the question
- Saying launch is fire-and-forget and the parent ignores it
- Claiming the parent completes the instant its lambda returns
- Believing you must manually join() every child for safety
- Thinking a failing child during Completing is ignored
- Confusing Completing (awaiting children) with Cancelling