One failing coroutine cancels its siblings

Read the explanation
What if

Inside coroutineScope, a throws. The scope cancels b, which is still in delay, and then rethrows a's exception.

Predict first

Inside coroutineScope, a throws while its sibling b is still in delay(1_000). What happens to b?

01234567coroutineScope: launch a, blaunchwaits for its childrencoroutineScope: rethrows: scope failed: a failedfaileddelay(50)coroutine a: throws IllegalStateExceptionthrowsdelay(1_000)coroutine b: cancelled: prints, rethrowscancelledcoroutine b: b done: nevernever

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
  • Error
  • Failed
  • Skipped
Step 1/5

coroutineScope launches two children, a and b.

1fun main() = runBlocking<Unit> {2    try {3        coroutineScope {4            launch { // a5                delay(50)6                throw IllegalStateException("a failed")7            }8            launch { // b9                try {10                    delay(1_000) // still working when a fails11                    println("b done")12                } catch (e: CancellationException) {13                    println("b cancelled")14                    throw e15                }16            }17        }18    } catch (e: IllegalStateException) {19        println("scope failed: ${e.message}")20    }21}

Explanation

What just happened

coroutineScope launched two children. When a threw, the scope was failed, and it cancelled b, which was still in delay(1_000). Cancellation reached b as a CancellationException thrown at its suspension point: it printed b cancelled and rethrew. Only when both children were done did the scope rethrow a’s exception.

Try the what-if switch

Why

Structured concurrency means a scope doesn’t finish until all its children have, and an error never leaves work running in the background. coroutineScope is right when the children are parts of one job. supervisorScope is right when they’re independent, like separate requests.

Watch out

Cancellation is cooperative: it’s delivered at suspension points. A child busy in a loop with no suspension points won’t notice until it reaches one.

Common questions

What happens when a child coroutine fails?

In coroutineScope, the failure cancels the scope and every other child. Once they've all finished, the scope rethrows the child's exception.

What's the difference between coroutineScope and supervisorScope?

In supervisorScope, a failing child doesn't cancel its siblings or the scope. Its exception goes to its CoroutineExceptionHandler, so handle each child's errors yourself.

Should I catch CancellationException?

Only to clean up, then rethrow it. A coroutine that catches it and carries on keeps running after it was cancelled.

Verified: this timeline matches a real program, checked in CI on Kotlin 2.4 with kotlinx.coroutines 1.11. How we verify. Unclear or wrong? Tell us