When a parent and several children fail around the same time, which exception is reported at the root, and what happens to the others?
answer
- first failure becomes the primary reported exception
- later failures attached via addSuppressed()
- exactly one exception reaches the handler
- teardown CancellationExceptions are filtered out
- racing winner is non-deterministic
basics
~20 sThe first exception that triggers cancellation is the one reported. Later exceptions from coroutines that were cancelled as a result are attached to it as suppressed exceptions, so they aren't lost but aren't reported separately.
solid answer
~40 sCoroutine cancellation is **first-failure-wins**: the exception that first cancels the scope becomes the *reported* (primary) exception at the root. As that cancellation tears down the rest of the tree, other coroutines may throw too — those subsequent exceptions are **attached as suppressed exceptions** (via `Throwable.addSuppressed`) on the primary, not reported independently. Exactly one exception reaches the `CoroutineExceptionHandler`. A subtlety: `CancellationException`s that arise purely from the teardown are ignored for reporting (they're normal), but a genuinely different failure that occurs during teardown is suppressed onto the original. This avoids exception storms while still preserving the secondary failures for debugging via `getSuppressed()`. You generally cannot rely on *which* concurrent failure wins if they race; design for the set of failures, not a specific one.
code
kotlin · 16 linesimport kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("primary: ${e.message}")
e.suppressed.forEach { println("suppressed: ${it.message}") }
}
val scope = CoroutineScope(Job() + handler)
scope.launch {
launch { throw IllegalStateException("first") }
launch {
try { delay(Long.MAX_VALUE) }
finally { throw RuntimeException("second") } // suppressed onto first
}
}.join()
}go deeper
Recognizes that only one error is usually reported even when several things fail.
Knows the first failure is primary and others are somehow attached.
Explains addSuppressed/getSuppressed and that teardown CancellationExceptions are filtered.
Reasons about non-determinism under racing failures and designs handling around failure sets and suppressed-exception inspection.
## The problem When a failure cancels a scope, it tears down many coroutines, and *several* of them may throw on the way down. If each were reported separately you'd get an exception storm and a confusing handler that fires repeatedly. Kotlin coroutines resolve this deterministically. ## First-failure-wins The **first** non-`CancellationException` that fails the scope becomes the **primary** exception — the one delivered to the `CoroutineExceptionHandler` (or thread handler) at the root. Exactly **one** exception is reported. ## Suppressed exceptions Additional exceptions thrown by other coroutines as they are cancelled are not discarded: they are **attached to the primary via `Throwable.addSuppressed(...)`**. You can later inspect them with `primary.getSuppressed()`. This is the same JVM mechanism `try`-with-resources uses. ```kotlin val handler = CoroutineExceptionHandler { _, e -> println("primary: ${e.message}") e.suppressed.forEach { println(" suppressed: ${it.message}") } } ``` ## CancellationExceptions are filtered out When the scope is being cancelled, most children just throw `CancellationException`. Those are **normal cancellation** and are *not* added as suppressed or reported — they are the expected consequence of teardown. Only genuine, *different* failures get suppressed onto the primary. This keeps the report focused on real errors. ## Racing failures If two coroutines fail at nearly the same instant, *which* one becomes primary depends on scheduling and is **not guaranteed**. Robust code should not branch on the identity of the winning exception when failures can race; instead handle the category of failure and consult `getSuppressed()` for the rest. ## Why this design - One clear primary cause → handler fires once, logs are readable. - No data loss → secondary causes preserved via suppression. - Cancellation noise filtered → teardown `CancellationException`s don't pollute the report. ## Practical implications When debugging a multi-coroutine failure, always check `getSuppressed()` — the *root cause* you see first may have several siblings hiding underneath it. And don't write logic assuming a specific exception always wins a race; treat the winner as one representative of a failure set.
- How do you recover the secondary failures that were not reported as primary?Call `getSuppressed()` (or the Kotlin `suppressed` extension) on the primary throwable in your handler or catch block; the additional failures are attached there.
- If two coroutines fail simultaneously, can you predict which is reported?No — the winner depends on scheduling and is not guaranteed. Design for the set of possible failures and inspect suppressed exceptions rather than relying on a specific one winning.
A pile-up on the highway: the first crash is logged as the cause; the follow-on collisions are noted in the report as related, not filed as separate accidents.
saying these in an interview costs you the question
- Assuming every failing coroutine's exception is reported separately
- Not knowing about addSuppressed / getSuppressed
- Believing the winning exception in a race is deterministic
- Thinking teardown CancellationExceptions are reported