After calling job.cancel(), can the coroutine still execute code? Explain the Cancelling vs Cancelled states and what cancelAndJoin() guarantees.
answer
- Active -> Cancelling -> Cancelled
- cancel() returns immediately
- finally/use run in Cancelling state
- cancelAndJoin waits for terminal state
- suspending cleanup needs NonCancellable
basics
~10 sYes. cancel() returns right away while the coroutine is still finishing — running finally blocks and cleanup. It's fully done only after it reaches the Cancelled state, which cancelAndJoin() waits for.
solid answer
~40 scancel() transitions the Job from Active to Cancelling and returns immediately. In the Cancelling state the coroutine is still alive: it can run finally blocks, use {} cleanup, and (if wrapped in NonCancellable) even suspend during cleanup. Only once that unwinding completes does the Job reach the terminal Cancelled state. So code absolutely can run after cancel(). Calling job.cancel() then continuing assumes nothing about completion. job.cancelAndJoin() (equivalent to cancel() then join()) suspends the caller until the Job is fully Cancelled — all cleanup done, children finished. isCompleted/isCancelled become true only at that terminal point. This is why you can't free shared resources right after cancel() without joining: the coroutine may still hold them mid-cleanup.
code
kotlin · 10 linesval job = launch {
try { delay(10_000) }
finally { println("cleanup") }
}
delay(50)
println(job.isActive) // true
job.cancel()
println(job.isCancelled) // true, but cleanup may be pending
job.join()
println(job.isCompleted) // true: terminal Cancelled statego deeper
Understands cancel() doesn't instantly finish the coroutine and join() waits for it.
Distinguishes Cancelling vs Cancelled and that finally runs during winddown; uses cancelAndJoin().
Reasons about the isActive/isCancelled/isCompleted flags and resource races, plus NonCancellable for suspending cleanup.
Designs shutdown sequences that correctly order cancellation, joining, and resource release across a component tree.
## Job lifecycle states A `Job` moves through states. The relevant ones for cancellation: - **Active** — running normally. - **Cancelling** — `cancel()` was called (or it failed); the coroutine is winding down but **not finished**. - **Cancelled** — terminal; fully wound down, children complete, cleanup done. Flags: `isActive` is true only while Active; `isCancelled` becomes true once cancellation begins; `isCompleted` becomes true at the terminal state (Cancelled or Completed). ## Code DOES run after cancel() `cancel()` returns immediately, putting the job in **Cancelling**. The coroutine then: 1. Throws `CancellationException` at the suspension point. 2. Unwinds the stack, running every `finally` block and `use { }` cleanup along the way. 3. Reaches its terminal **Cancelled** state. ```kotlin val job = launch { try { delay(10_000) } finally { println("cleanup runs in Cancelling state") } } delay(50) job.cancel() println("cancel() returned, cleanup may not have run yet") job.join() println("now definitely Cancelled") ``` Output ordering shows that `cancel() returned` can print **before** the cleanup, proving the coroutine is still executing. ## A subtlety: suspending during cleanup Inside the Cancelling state, the job is already cancelled, so any new suspend call (e.g. `delay`) will immediately throw again. To run **suspending** cleanup you must wrap it in `withContext(NonCancellable) { ... }` (a sibling topic) so it isn't cut short. ## What cancelAndJoin() guarantees `job.cancelAndJoin()` = `job.cancel()` followed by `job.join()`. `join()` suspends the **caller** until the Job reaches its terminal state. So after `cancelAndJoin()` returns, you are guaranteed: - the coroutine and all its children have finished, - all `finally`/cleanup has run, - it is safe to reuse or free shared resources. ```kotlin job.cancelAndJoin() // caller suspends until job is fully Cancelled closeSharedPool() // now safe ``` ## Why this matters Freeing a shared resource immediately after `cancel()` (without join) risks a race: the coroutine may still be using it during its cleanup. Always `join` (or `cancelAndJoin`) when subsequent steps depend on the coroutine being completely done.
- When does isCancelled become true vs isCompleted?isCancelled flips true as soon as cancellation starts (Cancelling); isCompleted becomes true only at the terminal state once unwinding/children finish.
- Why might suspending cleanup in a finally block fail after cancel()?The job is already cancelled, so a new suspend call throws CancellationException immediately; wrap it in withContext(NonCancellable).
saying these in an interview costs you the question
- Claims no code runs after cancel()
- Frees shared resources right after cancel() without joining
- Conflates isCancelled (start) with isCompleted (terminal)
- Thinks cancelAndJoin() and cancel() are interchangeable
- Expects suspending cleanup to just work after cancellation