skip to content

Parent/Child Job Hierarchy

A Job moves through active, completing, and cancelled states, and every coroutine's Job becomes a child of the enclosing one. Cancellation flows down the tree while completion waits for children — the two directions you need to keep straight.

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

questions

5

What is a Job in Kotlin coroutines, and what are the states in its lifecycle?

level: juniorimportance: must knowfreq 70%

answer

  1. launch -> Job, async -> Deferred (a Job)
  2. States: New/Active/Completing/Completed/Cancelling/Cancelled
  3. No enum: isActive / isCompleted / isCancelled
  4. Cancelled is also Completed (terminal)
  5. Job lives in CoroutineContext: coroutineContext[Job]

basics

~10 s

A Job is a handle to a running coroutine. It can be active, can complete, or can be cancelled. You can wait for it, cancel it, or check if it is still running.

solid answer

~40 s

A Job is the lifecycle handle for a coroutine, returned by launch (async returns a Deferred, a Job subtype). It moves through states: New (created but not started, e.g. with start = CoroutineStart.LAZY), Active (running), Completing (its body finished but it waits for children), Completed, Cancelling, and Cancelled. You inspect it via isActive, isCompleted, and isCancelled, control it with start(), cancel(), and join() (suspends until done), and react with invokeOnCompletion {}. A Job lives in the CoroutineContext, so a coroutine can read its own Job via coroutineContext[Job]. Most states are not directly observable as an enum — the three booleans together encode them.

code

kotlin · 10 lines
kotlin
val job = scope.launch(start = CoroutineStart.LAZY) {
    delay(100)
}
println(job.isActive)    // false (New)
job.start()
println(job.isActive)    // true (Active)
job.cancel()
job.join()
println(job.isCancelled) // true
println(job.isCompleted) // true (Cancelled is terminal)

go deeper

for a junior

Knows launch returns a Job and can name active/cancelled/completed and use cancel()/join().

for a middle

Explains the Completing state and that a cancelled Job is also completed; reads state via the three booleans.

for a senior

Distinguishes Job vs Deferred, knows the Job lives in the context, and uses invokeOnCompletion with its cause semantics.

for a principal

Reasons about why the API exposes orthogonal booleans rather than an enum, and how Completing underpins structured concurrency guarantees.

## What a Job is A `Job` is a cancellable handle to the lifecycle of a coroutine — a background unit of work. When you call `launch { ... }` you get back a `Job`; `async { ... }` returns a `Deferred<T>`, which **is a Job** (a subtype that also carries a result via `await()`). A `Job` is also stored in the coroutine's `CoroutineContext`, so the running coroutine can find it with `coroutineContext[Job]`. ## The lifecycle states A Job conceptually passes through these states: - **New** — created but not started yet. Only happens with `CoroutineStart.LAZY`. Call `start()` or `join()` to begin it. - **Active** — started and running (the default after `launch`). - **Completing** — the coroutine's own code finished, but it is **waiting for its children** to finish before it can complete. This is internal/transient. - **Completed** — fully done, including all children. - **Cancelling** — `cancel()` was called (or an exception was thrown); it is winding down. - **Cancelled** — finished because it was cancelled. Cancelled is a **terminal, completed** state. ## Observing the state There is no public enum. You read three boolean properties: ```kotlin val job = scope.launch { /* work */ } println(job.isActive) // true while running println(job.isCompleted) // true once finished (normally OR cancelled) println(job.isCancelled) // true if cancelled or failed ``` Note the subtlety: a **cancelled** Job has `isCompleted == true` AND `isCancelled == true`. "Completed" here means "reached a terminal state", not "succeeded". ## Controlling a Job - `start()` — begin a lazy job; returns `true` if it actually started it. - `join()` — a `suspend` function that suspends the caller until the job is completely done (does not throw on cancellation). - `cancel(cause)` — requests cancellation. - `invokeOnCompletion { cause -> ... }` — register a callback fired when the job completes; `cause` is `null` on normal completion, a `CancellationException` if cancelled, or the failure. ```kotlin val job = scope.launch(start = CoroutineStart.LAZY) { doWork() } // state: New job.start() // -> Active job.invokeOnCompletion { cause -> println("done, cause=$cause") } job.join() // suspend until terminal ``` Understanding these states is the foundation for parent/child links: a parent sits in **Completing** while it waits for children, which is what makes structured concurrency work.

  • Why is there no single State enum exposed?
    The library exposes three orthogonal booleans (isActive/isCompleted/isCancelled) because some states are internal/transient (Completing, Cancelling) and the combinations cover what callers actually need.
  • What does join() do if the job was cancelled?
    join() simply returns when the job reaches a terminal state; it does not re-throw the cancellation cause. await() on a Deferred, by contrast, re-throws the failure.

A Job is like a tracking number for a delivery: you can check its status, cancel it, or wait at the door until it arrives.

saying these in an interview costs you the question

  • Saying isCompleted is false for a cancelled job
  • Thinking launch returns a Deferred / async returns a plain Job
  • Claiming there is a public Job.State enum to switch on
  • Believing a coroutine starts Active when created with LAZY
  • Confusing join() (waits, no throw) with await() (waits, re-throws)

context

open as a page

Why does a parent coroutine not complete until its children finish, and what is the 'Completing' state?

level: middleimportance: must knowfreq 55%

basics

~20 s

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

open as a page

How does cancellation propagate through a Job hierarchy when you cancel a parent?

level: seniorimportance: must knowfreq 60%

basics

~10 s

Cancelling a parent cancels all of its children, and their children, all the way down the tree. Each coroutine stops at its next suspension point by getting a cancellation exception.

open as a page

What is the practical difference between a coroutine launched as a structured child versus one given its own root Job, and why does it matter for lifecycle?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A structured child is tracked by its parent: the parent waits for it and cancels it. A coroutine given its own fresh Job becomes independent, so the parent neither waits for it nor cancels it — risking leaks.

open as a page