liveBook · Manning

Structured concurrency

As explained in Kotlin in Action, 2E

Structured concurrency is a concurrency model that scopes concurrent tasks so their lifetimes and failures are managed through a shared parent-child hierarchy.

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

How Kotlin in Action, 2E covers it

How deep it goes
Develops scopes, Job-based hierarchy, cancellation, contexts, and error propagation; defers detailed exception behavior to later treatment.
Examples
Uses concurrent image loading, nested launch and delay tasks, and a heartbeat beside a failing coroutine.
What it assumes
Builds from Kotlin coroutines and suspending functions while introducing scope-based task lifecycle management.
What you can do after
Apply it via Kotlin coroutine scopes to decompose work, tie tasks to lifecycles, and choose failure boundaries.
How it connects
Connects coroutine scopes, suspending functions, async, await, cancellation, CoroutineContext, Job, SupervisorJob, and exceptions.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Kotlin in Action, 2E. The section links point to the parts of the book it draws on.