How are parent and child Jobs linked, and how does a coroutine become a child of another?
answer
- Link = Job element in CoroutineContext
- Builder replaces parent's Job with the child's Job
- Parent keeps refs to children -> a tree
- Pass Job() in builder => new root, link broken
- coroutineScope/supervisorScope make a sub-Job parent
basics
~10 sWhen you start a coroutine inside another coroutine or scope, the new coroutine's Job automatically becomes a child of the surrounding Job. The link is set up through the coroutine context.
solid answer
~40 sA child becomes linked to a parent through the CoroutineContext. When you call launch/async, the builder takes the Job from the receiving scope's context and uses it as the parent of the newly created Job. The new Job registers itself with the parent (the parent keeps references to children), and the child's context inherits everything else from the parent except the Job, which is replaced by the child's own Job. You can override the parent explicitly by passing a Job in the builder's context argument — that re-parents the coroutine. coroutineScope and supervisorScope create a new sub-Job that becomes the parent for everything launched inside. The whole tree is what enables structured concurrency: parent waits for children, and cancellation flows down the links.
code
kotlin · 11 linesfun CoroutineScope.demo() {
val parent = launch {
val a = launch { delay(1000) } // child of parent
val b = launch { delay(1000) } // child of parent
// parent stays in Completing until a and b finish
}
// Re-parenting: breaks the link with the scope
val orphan = launch(Job()) { delay(1000) }
// 'orphan' is now a root; cancelling the scope won't cancel it.
}go deeper
Knows launching inside a scope/coroutine makes a child automatically.
Explains the link is a Job in the context that the builder swaps, forming a tree, and inheritance of other context elements.
Recognizes launch(Job()) re-roots and breaks structure, and ties the tree to await/cancel guarantees.
Discusses leak implications of accidental re-parenting and how the context-element design keeps the model uniform across builders.
## The link lives in the context Every coroutine runs with a `CoroutineContext` — a typed map of elements (dispatcher, `Job`, `CoroutineName`, exception handler...). One of those elements is the **`Job`**. When you write `scope.launch { ... }`, the `launch` builder: 1. Reads the `Job` currently in the scope's context — call it the **parent Job**. 2. Creates a **new child Job** whose parent is that one. 3. Builds the child's context by taking the parent context and **replacing** its `Job` element with the new child Job (everything else — dispatcher, name — is inherited). So the parent/child relationship is just "the child's Job knows its parent, and the parent keeps a reference to each child". This forms a **tree** rooted at the scope's Job. ```kotlin val parent = scope.launch { // parent Job P val child = launch { // child Job C, parent = P // coroutineContext[Job] == C // coroutineContext[Job]!!.parent == P (conceptually) } } ``` ## Becoming a child You become a child by **launching inside something that carries a Job**: another coroutine's body, or a `CoroutineScope` you call the builder on. This is automatic — you do not wire it by hand. ## Overriding the parent You can pass a `Job` (or context) into the builder to change parenting: ```kotlin scope.launch(Job()) { ... } // NEW root Job: NOT a child of scope's Job ``` This **breaks** the structured link — the coroutine is now its own root and the scope will no longer wait for it or cancel it. This is a common accidental leak. ## Sub-scopes - `coroutineScope { }` / `supervisorScope { }` create a fresh sub-`Job` that becomes the parent of everything launched inside the block, and the block suspends until all of them complete. - `withContext(Job()) { }` likewise re-roots. ## Why it matters This tree is the backbone of **structured concurrency**: - The parent will not complete until all children complete (it sits in the *Completing* state). - Cancelling the parent cancels all descendants. - A child's uncaught failure cancels the parent (unless a `SupervisorJob` breaks that upward path — see the sibling topic). Knowing that the link is "just a Job in the context, swapped per coroutine" demystifies most coroutine behavior.
- What happens if you pass a fresh Job() to launch?The coroutine becomes a new root, not a child of the scope. The scope won't await or cancel it — a frequent source of leaks and unstructured concurrency.
- Does the child inherit the dispatcher and name from the parent?Yes. The child inherits the entire parent context except the Job element, which is replaced by the child's own Job. You can still override individual elements in the builder's context.
Like an org chart: each new hire is filed under their manager automatically; give them a brand-new Job() and you've made them their own CEO, reporting to no one.
saying these in an interview costs you the question
- Thinking parenting is something you must wire up manually each time
- Not realizing launch(Job()) detaches the coroutine from the scope
- Claiming the child reuses the parent's exact Job instance
- Believing the dispatcher is NOT inherited by children
- Saying the parent reference is the only direction (parent has no list of children)