skip to content

SupervisorJob

With a SupervisorJob a failing child is contained rather than taking down its siblings and parent. It is what you want for a long-lived scope serving independent requests, and it underpins supervisorScope.

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

questions

5

What is a SupervisorJob in Kotlin coroutines, and how does it differ from a regular Job when one child fails?

level: juniorimportance: must knowfreq 70%

answer

  1. Regular Job: child fails -> parent + all siblings cancelled
  2. SupervisorJob: child fails -> siblings + parent survive
  3. Cancellation still flows DOWN with supervisor
  4. supervisorScope { } = scope backed by SupervisorJob
  5. Does not swallow exceptions; still need a handler

basics

~10 s

A SupervisorJob is a special parent for coroutines. If one child crashes, the others keep running and the parent stays alive. With a regular Job, one crash cancels everything.

solid answer

~40 s

A Job is the handle that controls a coroutine's lifecycle and links parents to children. With a normal Job, failure cancellation propagates both ways: a failing child cancels its parent, which then cancels all sibling children. SupervisorJob() reverses the downward part: a child's failure is NOT propagated upward, so the parent and siblings keep running; cancellation still flows downward (cancelling the parent cancels all children). You install it via CoroutineScope(SupervisorJob() + Dispatchers.Default), or use the supervisorScope { } builder which creates a scope backed by a SupervisorJob for its block. This makes it the basis for resilient scopes where independent tasks must not take each other down — e.g. a UI scope handling several independent requests.

code

kotlin · 3 lines
kotlin
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
scope.launch { throw RuntimeException("boom") } // dies alone
scope.launch { delay(100); println("still alive") } // survives

go deeper

for a junior

Can state that a failing child does not cancel siblings or parent, unlike a regular Job.

for a middle

Adds that cancellation still flows downward and knows supervisorScope as the structured form.

for a senior

Explains that exceptions are still reported and require a handler; distinguishes propagation from handling.

for a principal

Frames it as the building block for resilient scopes and reasons about when downward-only failure isolation is the right contract.

## What a Job is Every coroutine has a `Job` — an object that represents its lifecycle (active, completing, cancelled, completed) and forms a **parent/child tree**. A coroutine launched inside a scope becomes a child of that scope's Job. This tree is what powers **structured concurrency**: parents wait for children, and cancellation flows through the tree. ## Normal Job: failure propagates BOTH ways With a regular `Job`, when a child coroutine **fails** (throws an uncaught exception, not a `CancellationException`): 1. The exception propagates **up** to the parent. 2. The parent cancels itself. 3. The parent then cancels **all its other children** (the failing child's siblings). So one crash takes down the whole scope. This is correct when the tasks are interdependent. ## SupervisorJob: failure does NOT propagate up `SupervisorJob()` is a `Job` whose children **fail independently**. A child's failure is not sent to the parent, so the parent and the siblings survive. Crucially, **cancellation still flows downward**: cancelling the SupervisorJob (or the scope) cancels all its children. ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val a = scope.launch { delay(50) throw RuntimeException("A failed") } val b = scope.launch { delay(200) println("B finished fine") // still runs } joinAll(a, b) } ``` With a plain `Job()` instead of `SupervisorJob()`, the failure of `a` would cancel `b`. ## How you use it - **`CoroutineScope(SupervisorJob() + dispatcher)`** — a long-lived scope (e.g. a service or ViewModel) where independent jobs must not kill each other. - **`supervisorScope { }`** — a suspending builder that creates a child scope backed by a SupervisorJob for the duration of the block, then waits for all children. This is the structured way to get supervisor behavior inside a function. ## Important caveat: exceptions still need handling A SupervisorJob does not *swallow* exceptions. An unhandled child exception is delivered to a `CoroutineExceptionHandler` (for `launch`) or re-thrown on `await()` (for `async`). If no handler is installed, it reaches the thread's uncaught-exception handler. SupervisorJob only changes **propagation among siblings**, not whether the exception is reported.

  • Does using a SupervisorJob mean child exceptions can be ignored?
    No. It only stops failure from cancelling siblings/parent. The exception is still reported — to a CoroutineExceptionHandler for launch, or on await() for async — so you must still handle it.

A regular Job is a string of fairy lights wired in series — one dead bulb kills the strand; a SupervisorJob wires them in parallel — one bulb dies, the rest stay lit.

saying these in an interview costs you the question

  • Saying SupervisorJob makes exceptions disappear / get swallowed
  • Claiming cancellation no longer flows downward
  • Thinking it changes async's await() rethrow behavior
  • Confusing it with try/catch error handling

context

open as a page

How does supervisorScope { } differ from coroutineScope { } with respect to child failures?

level: middleimportance: must knowfreq 65%

basics

~10 s

Both wait for all child coroutines to finish. In coroutineScope, one child failing cancels all the others and throws. In supervisorScope, a failing child does not cancel its siblings.

open as a page

Where do uncaught exceptions from launch children go under a SupervisorJob, and how do you handle them correctly?

level: seniorimportance: should knowfreq 50%

basics

~10 s

A launch child's uncaught exception under a SupervisorJob goes to a CoroutineExceptionHandler. The handler must be on the child (or its scope), not added when you call launch on a coroutineScope's child.

open as a page

A developer writes scope.launch(SupervisorJob()) { ... } expecting that child's failures to be isolated. Why is this often a mistake?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Passing a SupervisorJob into launch makes it the coroutine's parent job, which breaks structured concurrency: the new coroutine is no longer tied to the scope, so cancelling the scope won't cancel it.

open as a page

How would you design a long-lived service scope so that independent background tasks fail in isolation, are observable, and are all cancelled on shutdown?

level: principalimportance: nice to knowfreq 25%

basics

~10 s

Build the scope on a SupervisorJob plus a dispatcher and a CoroutineExceptionHandler so one task's failure doesn't kill the others. On shutdown, cancel the scope to stop everything, and log failures via the handler.

open as a page