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
- supervisorScope:
a’s failure goes to its handler, which printshandled: a failed.bisn’t cancelled; it finishes its delay and printsb done, and the scope ends normally.
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