skip to content

Given two children launched in the same scope, if one throws, what happens to the other? Walk through the Job-tree mechanics.

level: middleimportance: should knowfreq 55%

answer

  1. siblings share one parent Job
  2. failure cancels parent, parent cancels all children
  3. sibling gets CancellationException at next suspension point
  4. cooperative — stops only at a suspension/isActive check
  5. use supervisorScope to isolate

basics

~10 s

If one child fails with a real exception, the shared parent is cancelled, and the parent then cancels the other child too. So the sibling stops running.

solid answer

~40 s

Both children share one parent `Job`. A real (non-cancellation) exception in child A fails A's `Job`, which cancels the **parent**. Cancelling a `Job` cancels **all its children**, so child B receives a `CancellationException` at its next suspension point and stops. This is the core of structured concurrency: a failure is not isolated to one coroutine. The original exception keeps propagating to the scope root for reporting, while B's cancellation is normal (not re-reported). If you do NOT want this — you want B to keep running when A fails — you must isolate them under a `SupervisorJob`/`supervisorScope`, where a child failure does not propagate to the parent. Also note: cancellation of B is cooperative, so B only actually stops at a suspension point or an explicit `isActive`/`ensureActive()` check.

code

kotlin · 11 lines
kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    val parent = CoroutineScope(Job())
    val b = parent.launch {
        try { repeat(100) { delay(50) } }
        finally { println("B cancelled because A failed") }
    }
    parent.launch { delay(80); throw RuntimeException("A failed") }
    b.join()
}

go deeper

for a junior

Knows the sibling gets cancelled when one child fails.

for a middle

Walks the chain: child fails, parent cancelled, all children cancelled, sibling stops.

for a senior

Adds the cooperative-cancellation caveat and the supervisorScope opt-out.

for a principal

Discusses design trade-offs of all-or-nothing vs supervised isolation and how reporting vs cancellation differ at the root.

## The setup ```kotlin val parent = CoroutineScope(Job()) val a = parent.launch { delay(100); throw RuntimeException("A failed") } val b = parent.launch { repeat(1000) { delay(50); println("B working") } } ``` Here `a` and `b` are **siblings**: both their `Job`s are children of `parent`'s `Job`. ## Step-by-step propagation 1. After 100 ms, `a` throws `RuntimeException` — a *non-cancellation* exception, i.e. a **failure**. 2. `a`'s `Job` enters the failed state and **cancels its parent** with that exception. 3. Cancelling the parent `Job` **cancels every child** of that parent. `b` is a child, so `b` gets cancelled. 4. `b` receives a `CancellationException` thrown at its next suspension point (`delay`), so it stops — its cancellation is *normal*, not reported as an error. 5. The original `RuntimeException` continues to the scope root and is **reported** there. So the answer: **the sibling is cancelled.** Failure is not isolated. ## Cooperative cancellation caveat `b` does not stop *instantly*. Cancellation sets a flag; `b` only actually exits when it hits a cancellation check — a suspension point (`delay`, `yield`, `withContext`) or an explicit `ensureActive()` / `isActive` test. A tight CPU loop with no such checks would keep running: ```kotlin parent.launch { while (true) { /* no suspension */ } } // ignores cancellation ``` Add `yield()` or check `isActive` to make it cooperative. ## How to opt out: supervisor scoping If you want `b` to survive `a`'s failure, isolate them: ```kotlin supervisorScope { launch { throw RuntimeException("A failed") } // does NOT cancel sibling launch { /* keeps running */ } } ``` Under a `SupervisorJob`, a child's failure does not propagate to the parent, so siblings are unaffected (the detailed supervisor semantics are their own topic). ## Mental model A regular `Job` enforces all-or-nothing: children live and die together. That is the default precisely so that a partial failure doesn't leave half-finished work silently running.

  • Why might the sibling not stop immediately when cancelled?
    Cancellation is cooperative: the sibling only throws CancellationException at a suspension point or an explicit isActive/ensureActive() check. A tight loop with no such check keeps running.
  • How would you keep the sibling running despite the failure?
    Launch both under a supervisorScope or a CoroutineScope(SupervisorJob()), where a child's failure does not propagate to the parent, so siblings are unaffected.

One climber falling pulls the whole roped team unless they're on separate ropes (a supervisor scope).

saying these in an interview costs you the question

  • Claiming the sibling keeps running by default
  • Forgetting cancellation is cooperative and assuming instant stop
  • Not connecting parent cancellation to child cancellation
  • Confusing this default with supervisor behavior

context