Structured concurrency organizes coroutines into a parent-child hierarchy so concurrent work has a known lifetime. Rather than manually tracking every task, a parent scope tracks its children, waits for them, and can cancel work that is no longer useful. This prevents rogue coroutines, resource leaks, and wasted work after a request or screen has gone away. section 15.0 section 15.1
suspend fun loadAndOverlay(first: String, second: String): Image =
coroutineScope {
val firstDeferred = async { loadImage(first) } #1
val secondDeferred = async { loadImage(second) } #2
combineImages(firstDeferred.await(), secondDeferred.await()) #3
}
Use coroutineScope to split a result into concurrent child operations. async starts both image loads from the same scope; await collects both results before combineImages returns. section 1.2.4 section 15.3
A structured scope supplies several practical guarantees:
- Coroutines launched with launch or async inside another coroutine builder become children of that scope. section 15.1
- A parent does not complete until its children and descendants have completed, without manual await calls for launched children. section 15.1
- Cancelling a parent cancels its children, allowing a whole operation to stop when its result is no longer needed. section 14.3 section 15.3
- For concurrent decomposition, an uncaught child failure fails the parent and cancels sibling work because no complete result remains. section 18.2
A parent waits for nested work
A runBlocking scope launches two children. One child launches a grandchild after a delay. Although the parent body prints “Parent done!” first, runBlocking returns only after both children and the grandchild finish. The hierarchy is established by scope membership rather than by manually storing task handles. section 15.1
Failure policy depends on the job in the coroutine context. A regular Job treats a child failure as failure of the shared operation. A SupervisorJob instead lets a parent supervise children whose individual failures should be handled without bringing down the parent, such as independent server jobs or UI refresh work. section 18.2 section 18.2.2
Listing 18.3 Catching the exception inside the coroutine
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
fun main(): Unit = runBlocking {
launch {
try {
while (true) {
println("Heartbeat!")
delay(500.milliseconds)
}
} catch (e: Exception) {
println("Heartbeat terminated: $e")
throw e
}
}
launch {
try {
delay(1.seconds)
throw UnsupportedOperationException("Ow!")
} catch(u: UnsupportedOperationException) {
println("Caught $u")
}
}
}
This example catches a child’s UnsupportedOperationException inside that child, so the heartbeat sibling continues. Catching an exception locally changes whether it crosses the coroutine boundary. section 18.2.2
Cancellation is normal control flow in coroutine lifecycles. It is delivered at suspension points through CancellationException, so broad exception handling must not swallow cancellation; rethrow it or avoid catching it. GlobalScope should not be used for application work because it breaks the lifetime structure. section 15.3 section 15.3
