thecodex.expert · The Codex Family of Knowledge
Kotlin

Coroutines Deep Dive

A parent coroutine scope genuinely cannot finish until every single coroutine launched inside it has completed — structured concurrency makes an orphaned, forgotten background task structurally impossible.

Kotlin 2.1 Structured concurrency Last verified:
Canonical Definition

Every coroutine launched with launch or async runs inside a CoroutineScope, and that scope forms a parent-child relationship with every coroutine it starts — a parent coroutine's own completion genuinely waits for all its children to finish first. Cancelling a scope (or its parent Job) cancels every child coroutine within it, cooperatively — a suspending function checks for cancellation at its suspension points and stops itself, rather than being forcibly killed. This structured relationship is precisely what prevents the class of bugs where a background task keeps running long after the code that started it has moved on or been abandoned.

coroutineScope: a parent that waits for every child

Both launched coroutines run concurrently, but coroutineScope itself doesn't return until BOTH have completed — the parent structurally cannot finish early and leave a child still running unattended.

Kotlinstructured_concurrency.kt
suspend fun loadData() = coroutineScope {   // a genuine parent scope
    launch {
        delay(1000)
        println("First task done")
    }
    launch {
        delay(500)
        println("Second task done")
    }
}   // coroutineScope waits here until BOTH children have finished — guaranteed

Cancellation: cooperative, not forced

Cancellation isn't a forced kill — it works because delay (and other suspending functions) check for cancellation at their suspension points and throw a CancellationException there, which propagates up naturally, stopping the coroutine cooperatively rather than at an arbitrary instruction.

Kotlincancellation.kt
val job = CoroutineScope(Dispatchers.Default).launch {
    repeat(1000) { i ->
        println("Working: $i")
        delay(100)   // cancellation is checked HERE, at each suspension point
    }
}

delay(300)
job.cancel()   // cooperatively stops the loop the next time it hits delay()

Cancelling a parent cancels every child

Cancelling the parent Job cancels both child coroutines automatically — there's no need to track and individually cancel each one, since structured concurrency propagates cancellation down the whole hierarchy on its own.

Kotlinparent_cancels_children.kt
val parentJob = CoroutineScope(Dispatchers.Default).launch {
    launch { delay(5000); println("Child 1 done") }
    launch { delay(5000); println("Child 2 done") }
}

delay(100)
parentJob.cancel()   // BOTH children are cancelled automatically — neither line ever prints

Sources

1
JetBrains. Kotlin Documentation, "Coroutine basics" and "Cancellation and timeouts," kotlinlang.org/docs/coroutines-basics.html.