Course outline

Kotlin / Concurrency, interoperability, and integrated design / Lesson 14

Preserve cancellation and design failure ownership

Use cooperative cancellation, finally cleanup, timeout boundaries, and supervision without swallowing lifecycle signals.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain why cancellation is a request that code must observe.
  • Distinguish fail-together scopes from independent-child supervision.

Cancellation does not forcibly stop arbitrary code

Coroutine cancellation is cooperative. Suspending library operations often check cancellation, while a long CPU loop or blocking call may continue unless it uses a supported cancellation mechanism. ensureActive checks the current scope or context and throws when cancellation applies; yield can also give other work an opportunity to run. Choose check frequency according to the work and responsiveness requirement rather than inserting a universal magic interval. A cancellation request also does not roll back an external action that already committed, so state-changing operations need a separate outcome and recovery design.

Join cancellation before assuming cleanup has finished

cancel requests cancellation, while cancelAndJoin also waits for completion. The example starts its child undispatched so the initial message and suspension happen before the caller cancels it. Finally prints a cleanup marker and the parent prints only after joining. There is no timed race or external resource here. Real cleanup should be bounded and preserve cancellation; suspending cleanup in a cancelled context needs careful narrowly scoped handling where supported. Do not use broad non-cancellable regions to keep ordinary business work alive after its owner has ended.

kotlin A deterministic cancellation-order example
import kotlinx.coroutines.*

fun main() = runBlocking {
    val child = launch(start = CoroutineStart.UNDISPATCHED) {
        try {
            println("started")
            awaitCancellation()
        } finally {
            println("cleanup")
        }
    }
    child.cancelAndJoin()
    println("cancelled and joined")
}
Expected output
started
cleanup
cancelled and joined

Expected lines are started, cleanup, cancelled and joined. Compatible kotlinx.coroutines is required. The program was not compiled or run, and the marker is not evidence of real resource closure.

Do not turn CancellationException into ordinary success

CancellationException is a lifecycle signal used by coroutine cancellation. A broad catch of Exception can intercept it, and helpers that capture arbitrary exceptions can do the same. If translating ordinary failures, rethrow cancellation explicitly before handling other exceptions. Otherwise the caller may wait unnecessarily or receive a fabricated success after it requested cancellation. A finally block should release owned resources and preserve the original control flow. Catching Throwable is even broader and can hide failures the application is not equipped to recover from. Narrow the boundary to failures it genuinely understands.

A timeout cancels a scope rather than undoing the world

withTimeout reports a TimeoutCancellationException when its deadline applies; withTimeoutOrNull offers a nullable timeout result. The block must still cooperate with cancellation. A zero or negative timeout immediately declines to execute its block, which makes the following demonstration deterministic without sleeping. A timeout can race with returning a resource, so acquire, use, and close resources inside an appropriate ownership scope. Do not claim that a timed-out remote reservation never happened or retry it under a new identity without checking its business outcome.

kotlin A nonpositive timeout boundary
import kotlinx.coroutines.*

fun main() = runBlocking {
    val result = withTimeoutOrNull(0L) {
        "block result"
    }
    println(result ?: "timed out before block")
}
Expected output
timed out before block

Expected output is timed out before block. The block is not executed for the declared zero timeout. This is reasoned API behavior, not an executed timing experiment.

Supervise independent work without ignoring its errors

coroutineScope fits subtasks that must succeed together. supervisorScope can keep sibling children independent when one child fails, but it does not automatically consume every exception. A supervised launch can still have an unhandled failure, and a supervised async still needs its result or failure observed. The supervisor block’s own failure cancels its children. Use supervision when the product genuinely permits partial results and define how each failure is represented. It is not a general switch that makes an application resilient, nor does it remove the need for lifecycle cancellation and resource bounds.

Common errors and fixes

Cancellation is caught and returned as an empty success.

Why it happens
A broad exception translator swallowed the lifecycle signal.
How to fix it
Rethrow CancellationException and handle only intended ordinary failures.
Prevent it next time
Include cancellation cases when reviewing catch and result helpers.

A CPU loop ignores cancellation.

Why it happens
It never suspends or checks the active context.
How to fix it
Add cooperative checks at a justified work boundary.
Prevent it next time
State responsiveness and resource limits for long computations.

Supervision leaves a child error unhandled.

Why it happens
Failure independence was mistaken for automatic error consumption.
How to fix it
Observe async results and handle launch failures through an owned policy.
Prevent it next time
Specify partial-result semantics before adding supervision.

Check your understanding

1. Why is cancel followed immediately by reading cleanup state unreliable?

Show worked solution

cancel requests cancellation but does not guarantee the child has completed its finally block at that instant. Use an appropriate join when the owner must wait, and design cleanup visibility and resource ownership explicitly. Even joined coroutine completion does not prove an external system reversed a committed effect.

2. When should two report sections use supervisorScope instead of fail-together behavior?

Show worked solution

When the product allows one section to remain useful if the other fails and the application explicitly represents the missing section and its error. Observe each result, preserve cancellation of the overall report, and bound resource use. If both sections are required for correctness, independent survival can produce a misleading incomplete success.

Key takeaways

  • Cancellation is cooperative and must remain visible through error handling.
  • Supervision changes failure relationships and requires an explicit product policy.

Official references