skip to content

What is the difference between a regular Job and a SupervisorJob when one of their child coroutines fails?

level: juniorimportance: must knowfreq 70%

answer

  1. Regular Job = bidirectional cancellation, both ways
  2. SupervisorJob = no upward propagation, siblings survive
  3. Downward cancel still works for supervisor
  4. supervisorScope { } vs coroutineScope { }
  5. Child still needs its own try/catch or handler

basics

~10 s

With a normal Job, if one child crashes, the parent and all the other children are cancelled too. With a SupervisorJob, a crashing child only affects itself; its siblings keep running.

solid answer

~40 s

A regular Job uses bidirectional cancellation: when a child fails with an uncaught exception, it cancels its parent, which then cancels all sibling children. A SupervisorJob (or supervisorScope) reverses the upward direction: a child's failure is NOT propagated to the parent, so siblings keep running. Downward cancellation still works — cancelling the supervisor cancels all children. Practically you create one via SupervisorJob() in a CoroutineScope, or via the supervisorScope { } builder. Each child must handle its own failures (try/catch around launch's body, or a CoroutineExceptionHandler installed in the child's context), otherwise the exception still surfaces. SupervisorJob is ideal for UI scopes or servers where independent tasks must not take each other down.

code

kotlin · 9 lines
kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    supervisorScope {
        val a = launch { throw RuntimeException("A fails") }
        val b = launch { delay(200); println("B survives") }
        a.join(); b.join()
    }
}

go deeper

for a junior

Knows the one-line difference: regular Job cancels everyone, SupervisorJob keeps siblings alive.

for a middle

Distinguishes upward vs downward propagation and names supervisorScope vs coroutineScope.

for a senior

Adds that the exception is still uncaught and must be handled per-child, and picks the right scope for a real component.

for a principal

Frames it as a structured-concurrency policy decision and reasons about failure-isolation boundaries across an app's scope hierarchy.

## The core idea In structured concurrency, coroutines form a parent-child tree. A **Job** is the handle that represents a coroutine's lifecycle and its place in that tree. How failures travel through the tree depends on whether the parent is a normal **Job** or a **SupervisorJob**. ## Regular Job: failure cancels everything With a normal `Job`, exception propagation is **bidirectional**: - A child that throws an **uncaught** exception cancels itself, then **propagates the failure upward** to its parent. - The parent, on receiving a child failure, cancels **itself** and therefore **all of its other children** (downward). So one failing child takes down the whole sibling group. This is intentional: if a sub-task of a larger job fails, the rest of that job is usually meaningless. ## SupervisorJob: failure stays local A **`SupervisorJob`** changes only the **upward** direction: a child's failure is **not** propagated to the supervisor parent. Therefore siblings are **not** cancelled and keep running. The **downward** direction is unchanged — cancelling the supervisor still cancels all children. ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { // supervisorScope: child failure contained to that child supervisorScope { launch { delay(100) throw RuntimeException("boom in child A") } launch { delay(300) println("child B finished") // still prints } } } ``` With a plain `coroutineScope { }` instead, child B would be cancelled the moment child A throws. ## Two ways to get supervisor behaviour - **`supervisorScope { }`** — a suspending builder that creates a child scope whose Job is a supervisor. Preferred for a bounded block. - **`CoroutineScope(SupervisorJob())`** — build a long-lived scope (e.g. a UI/ViewModel scope) backed by a `SupervisorJob()` factory function. ## You still must handle the exception Supervisor only stops *sibling* cancellation. The exception itself is still **uncaught** unless you handle it. For a `launch`-ed child you handle it by either a `try/catch` inside the child, or a `CoroutineExceptionHandler` in the child's context. ## Key APIs - `Job` / `SupervisorJob()` — the two Job factories. - `coroutineScope { }` vs `supervisorScope { }` — the two scoping builders. - `launch`, `async`, `CoroutineExceptionHandler`.

  • If a child under a SupervisorJob throws and you do nothing, what happens?
    Siblings keep running, but the exception is still uncaught — it goes to the child's CoroutineExceptionHandler if present, otherwise to the default handler (which crashes the thread/process for launch on the JVM).
  • Does cancelling a SupervisorJob cancel its children?
    Yes. Supervisor only blocks the upward (child-to-parent) direction; downward cancellation is unchanged.

A normal Job is a string of fairy lights — one blown bulb kills the whole string; a SupervisorJob gives each bulb its own fuse.

saying these in an interview costs you the question

  • Saying SupervisorJob makes exceptions disappear / be swallowed automatically
  • Claiming downward cancellation also stops working with a supervisor
  • Thinking coroutineScope and supervisorScope are interchangeable
  • Believing siblings are cancelled under a SupervisorJob

context