Structured concurrency gives child coroutines an owner and a lifetime. Failure propagation is part of that structure: in an ordinary scope, a failing child can cancel siblings. Sometimes that is the correct response because all results are needed together. Sometimes independent panels or optional sources should fail separately.
The coroutines lesson teaches launch, async and cancellation. This reference focuses on choosing a failure policy and preserving cleanup. Examples require a Kotlin/JVM project with kotlinx-coroutines-core 1.10.2, such as an implementation dependency from Maven Central. They use runBlocking only as a console entry point.
Supervision changes child failure propagation
SupervisorScope creates a structured scope in which one child's failure does not automatically cancel sibling children. It still waits for its children and remains tied to its parent. If the supervising block itself fails or its parent is cancelled, its children are cancelled. Supervision does not create detached work with unlimited lifetime.
Use supervision for genuinely independent results, not as a universal way to hide errors. A checkout requiring inventory and payment should have an explicit transactional policy; merely supervising both does not make a half-completed operation safe. An optional weather panel and a separate news panel can more naturally have separate failures.
Async stores its result or failure in Deferred. Await observes that result and rethrows a failure. Within supervision you still need to await and handle failed deferred values. Launch has different exception-reporting behavior because it has no return value to await; choose its handler and ownership deliberately rather than assume supervisorScope catches everything.
Cancellation is cooperative control flow
Cancellation is usually communicated through CancellationException at suspending operations or explicit checks. A CPU-intensive loop with no suspension or checks can delay cancellation. Add ensureActive or another appropriate cooperative point when long-running work must respond promptly.
Do not turn CancellationException into a successful fallback in a broad catch. If catching Exception for a boundary operation is necessary, rethrow cancellation before handling ordinary failures. Otherwise a cancelled task can keep publishing results or continuing work after its owner has left. RunCatching also catches exceptions, so using it around suspending work requires the same care.
Cancellation is not automatic rollback of external effects. A remote update may already have completed when the caller stops waiting. Design operations with idempotency, compensation or explicit transaction semantics where the domain requires them. Supervision and cancellation govern coroutine lifetimes; they do not reverse a database write by themselves.
A test can separate these concerns by using a fake service that records when an effect occurs and a signal that controls when the result returns. Cancel after the effect but before the response, then verify the application's reconciliation policy. This exposes a real boundary case that simply adding a delay or asserting that a Job is cancelled would miss.
Cleanup belongs in finally with a bounded policy
A finally block runs as the coroutine unwinds, making it suitable for releasing ordinary resources. Non-suspending cleanup can often execute directly. Suspending cleanup in a cancelled coroutine may immediately encounter cancellation, so withContext(NonCancellable) can provide a narrowly scoped region when completion of that cleanup is required.
Keep that region short and bounded. Do not move the whole business operation into NonCancellable to defeat the caller's cancellation. Never use NonCancellable as the parent of a fresh launch or async to escape the structured job hierarchy. Its useful pattern here is a local withContext around necessary suspending cleanup.
Prefer resource APIs with clear close contracts and avoid complicated cleanup when a synchronous close suffices. If cleanup can fail, decide how to preserve the original failure and report the cleanup failure without replacing the most useful cause. Tests should include cancellation at the resource boundary, not only successful completion.
Official references
Read the supervisorScope contract and NonCancellable documentation. The official exception-handling guide connects builders, handlers and supervision. Review success, independent failure, parent cancellation and cleanup failure as separate behaviors before choosing the scope policy.
Kotlin Coroutine Supervision and Cleanup Examples
Each example shows the code, the output it produces and what each part does.
Observe independent async results
The output order follows the explicit print sequence, not a claim about which coroutine finishes first. The sibling result survives the optional child's failure. The catch handles a specific expected demonstration failure and does not swallow cancellation or every possible defect.
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
fun main() = runBlocking {
supervisorScope {
val optional = async<String> { throw IllegalStateException("optional unavailable") }
val required = async { "stock ready" }
val optionalText = try {
optional.await()
} catch (error: IllegalStateException) {
"optional failed"
}
println(optionalText)
println(required.await())
}
}optional failed
stock readyKotlin Coroutine Supervision and Cleanup in Real Projects
Worked exercise: Cancel after startup and finish cleanup
The worker confirms it has started, waits until cancellation, and then completes a small simulated suspending cleanup. A deferred signal makes the sequence deterministic without relying on a guessed delay before cancellation.
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
fun main() = runBlocking {
val started = CompletableDeferred<Unit>()
val worker = launch {
try {
println("started")
started.complete(Unit)
awaitCancellation()
} finally {
withContext(NonCancellable) {
delay(1)
println("cleanup complete")
}
}
}
started.await()
worker.cancelAndJoin()
println("joined")
}started
cleanup complete
joinedCancelAndJoin requests cancellation and waits for completion, including the finally block. The tiny delay stands in for necessary suspending cleanup; real cleanup should have its own finite contract. Removing the startup signal and racing cancellation against launch would make the printed sequence unreliable because the body might never start.
Kotlin Coroutine Supervision and Cleanup FAQ
Does supervision mean exceptions are ignored?
No. It changes the relationship between sibling failures, not the need to observe and handle errors. A failed async still throws when awaited, and a failed launch needs appropriate exception handling for its context. Supervision is useful when one independent result should remain available despite another's failure. Document those independent results and their fallback policy; otherwise a supervisor can conceal a mistaken assumption that every operation succeeded.
Can cancelling the parent leave supervised children running?
A properly structured supervisorScope remains a child of its caller. Parent cancellation cancels the scope and its children; supervision does not grant immunity from that cancellation. This is different from creating detached work in an unrelated global scope. Keep work tied to a lifecycle owner and wait for it where appropriate. The supervision decision concerns child failure isolation, while the parent still controls the overall lifetime.
Why should CancellationException usually be rethrown?
It signals that the coroutine's owner wants the operation to stop. Converting it into an ordinary successful result can defeat structured cancellation and allow later code to run under false assumptions. Handle cleanup in finally and then let cancellation propagate. If a broad catch is unavoidable for an external API, check for cancellation first and rethrow it before mapping ordinary failures to a domain error or user-facing fallback.
When is NonCancellable appropriate?
Use it narrowly with withContext when a cancelled coroutine must finish necessary suspending cleanup, such as a bounded protocol shutdown. It is unnecessary for ordinary synchronous release operations and inappropriate for shielding an entire business request indefinitely. Define a finite cleanup policy and consider failure reporting. Launching new unrelated work with NonCancellable can break the expected parent-child lifecycle, so prefer the local cleanup pattern shown in the exercise.
Kotlin Coroutine Supervision and Cleanup Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Where should a necessary bounded suspending cleanup run after cancellation?
Kotlin Coroutine Supervision and Cleanup Quiz
2 questions to check what you learned.
In supervisorScope, what happens when one async child fails?
- Siblings are not cancelled solely by that child failure, but await still observes the failure.
- The parent can never cancel any child.
- Its failure becomes a successful default automatically.
Why is a broad catch around suspending work potentially dangerous?
- All suspending functions are guaranteed never to throw.
- It may swallow CancellationException and defeat the owner's stop request.
- Catch always disables every finally block.