Structured Concurrency
Structured concurrency means coroutines form a parent-child tree: a parent does not complete until its children do, and cancelling a parent cancels its children. This prevents leaked coroutines and makes error handling predictable.
The scope tree
suspend fun loadAll() = coroutineScope { // parent scope
val a = async { loadA() } // child
val b = async { loadB() } // child
a.await() + b.await()
} // does not return until both children complete
If loadA() throws, loadB() is cancelled and loadAll() rethrows — no
dangling work.
Cancellation is cooperative
Cancellation does not force-stop a coroutine; the coroutine must cooperate by
suspending (most suspend functions check for cancellation) or checking
isActive:
val job = launch {
while (isActive) { // stops promptly when cancelled
computeChunk()
}
}
delay(100)
job.cancelAndJoin()
Always let CancellationException propagate — do not swallow it in a
catch (e: Exception).
Timeouts
val result = withTimeoutOrNull(1000) { // null if it takes too long
slowOperation()
}
withTimeout(1000) { slowOperation() } // throws TimeoutCancellationException
Exception propagation: SupervisorJob
By default, a failing child cancels its siblings. A supervisor scope isolates failures so one child’s failure does not cancel the others:
supervisorScope {
launch { mightFail() } // if this fails...
launch { keepsRunning() } // ...this one continues
}
Handle uncaught failures in launch with a CoroutineExceptionHandler:
val handler = CoroutineExceptionHandler { _, e -> println("caught $e") }
scope.launch(handler) { error("boom") }
Exercises
-
Use
withTimeoutOrNullto give a slowsuspendfunction 50 ms and return a fallback value when it does not finish.Solution
suspend fun loadOrFallback(): String =
withTimeoutOrNull(50) { slowLoad() } ?: "fallback"
</details>
- Show that a failure in one
asyncchild cancels a sibling withincoroutineScope.
This solution is in examples/core/part6/ and is tested by CI.
Previous: Scopes, Context & Dispatchers · Next: Asynchronous Streams with Flow