Does SupervisorJob prevent parent cancellation when a child coroutine fails?
0 reputation · 01 May 2026, 10:04 UTC
0 reputation · 01 May 2026, 10:04 UTC
In Kotlin Coroutines, structured concurrency typically ensures that a failure in a child coroutine propagates upward, triggering the cancellation of the parent and all remaining sibling coroutines. This behavior is designed to prevent orphaned tasks when a critical dependency fails.
To decouple this failure propagation, SupervisorJob is often employed. While it is documented that a supervisor does not fail its parent or siblings, the specific interaction between a SupervisorJob and a CoroutineExceptionHandler in a root scope requires clarification to ensure exceptions are handled without crashing the application.
Which mechanism is required to ensure that an uncaught exception in a child of a SupervisorJob is captured without triggering the cancellation of the supervisor itself? Does the CoroutineExceptionHandler need to be defined at the scope level or the individual job level to achieve this?
35275 reputation · 01 May 2026, 16:13 UTC
Yes. A SupervisorJob prevents a failing child coroutine from cancelling its parent and its siblings. In standard structured concurrency, a child's failure triggers a bidirectional cancellation: the child fails the parent, and the parent subsequently cancels all other children. A SupervisorJob overrides this behavior, ensuring that failure only propagates downwards. The supervisor remains active even if one or more of its children fail.
While a SupervisorJob prevents the cancellation of the parent, it does not automatically handle the exception. An uncaught exception in a child coroutine will still be treated as an uncaught exception by the system (potentially crashing the application) unless a CoroutineExceptionHandler is provided.
To ensure an exception is captured without triggering a crash, the CoroutineExceptionHandler must be defined at the scope level or the root coroutine launched by the supervisor.
CoroutineScope context, all children launched within that scope will delegate their uncaught exceptions to this handler.launch call.Crucial Note: Adding a CoroutineExceptionHandler to a child coroutine that is not a root coroutine (i.e., a child of another child) is generally ignored. The child delegates the exception to its parent, and the propagation continues upward until it reaches the supervisor or the scope.
To verify this behavior in your environment (assuming Kotlin Coroutines 1.6+), you can use the following pattern:
val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception") }
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
throw RuntimeException("Child 1 failed")
}
scope.launch {
delay(100)
println("Child 2 still running")
}
If "Child 2 still running" prints to the console, the SupervisorJob is functioning correctly. If the handler prints the exception, the CoroutineExceptionHandler is correctly positioned.
launch. Exceptions in async blocks are encapsulated in the Deferred object and are only thrown when await() is called, regardless of the SupervisorJob.supervisorScope { ... } is a scoping function that creates a temporary supervisor; it behaves similarly but is used for localizing failure within a specific block of code rather than defining a long-lived scope.Diagnostic Detail Needed: Are you using launch or async for these child tasks? The mechanism for capturing the exception differs significantly between the two.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.