What you'll learn
Async code is only trustworthy if it stops cleanly and fails predictably. This module covers the lifecycle side of coroutines: how parents own children, how cancellation works cooperatively, how to enforce timeouts, and how exceptions flow through a coroutine hierarchy.
By the end you'll be able to:
- Explain parent-child coroutine relationships
- Cancel coroutines and write cancellation-friendly code
- Release resources safely with
try/finally - Apply timeouts and handle uncaught exceptions
Parent-child relationships
When you launch a coroutine inside another, the new one becomes a child. The parent won't complete until all its children have completed, cancelling the parent cancels every child, and (by default) a failing child cancels the parent and its siblings. This hierarchy is the backbone of structured concurrency — it means work is never accidentally orphaned.
Key idea
Cooperative cancellation
Cancellation in Kotlin is cooperative: calling cancel() doesn't forcibly kill a coroutine — it flags it, and the coroutine notices at its next suspension point(any suspend call like delay, or a check of isActive). Well-behaved suspending code becomes cancellable automatically:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: $i")
delay(100L) // delay is a cancellation point
}
}
delay(350L)
println("cancelling")
job.cancelAndJoin() // cancel, then wait for it to stop
println("cancelled cleanly")
}Watch out
yield(), checking isActive, or calling ensureActive() periodically.Cleaning up & CancellationException
Under the hood, cancellation works by throwing a CancellationException from the suspension point. That means ordinary try/finally runs on cancellation, giving you a reliable place to close files, release locks, or roll back:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: $i")
delay(100L)
}
} finally {
// cancellation throws CancellationException — finally still runs
println("releasing resources")
}
}
delay(250L)
job.cancelAndJoin()
println("done")
}Note
CancellationException is special — the machinery treats it as normal, expected shutdown, not an error, so don't catch it and swallow it. If you need to suspend during cleanup (rare), wrap that part in withContext(NonCancellable).Timeouts
Real systems must not wait forever. withTimeout cancels the block and throws if it takes too long; withTimeoutOrNull returns null instead — usually the friendlier choice:
import kotlinx.coroutines.*
fun main() = runBlocking {
val result = withTimeoutOrNull(300L) { // give up after 300ms
repeat(1000) { i ->
println("working $i")
delay(100L)
}
"finished"
}
println("result: $result") // null — it ran out of time
}Exception handling
For async, exceptions surface when you call await(), so wrap that in try/catch. For launch, an uncaught exception propagates up the hierarchy; to handle it at the top, install a CoroutineExceptionHandler on the scope (pair it with a SupervisorJob so siblings aren't cancelled):
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("caught: ${e.message}")
}
// A top-level coroutine with a handler + SupervisorJob
val scope = CoroutineScope(SupervisorJob() + handler)
val job = scope.launch {
throw RuntimeException("boom")
}
job.join()
}Recap & quick check
Key takeaways
- Children belong to their parent: cancelling the parent cancels children, and the parent waits for them.
- Cancellation is cooperative — a coroutine reacts at its next suspension point (delay, yield, isActive).
- Cancellation throws CancellationException, so try/finally runs — use it to release resources.
- withTimeout throws on timeout; withTimeoutOrNull returns null instead.
- Handle async failures at await(); handle launch failures with a CoroutineExceptionHandler on the scope.
Quick check
1. What does 'cooperative cancellation' mean?
2. Why might a tight CPU loop ignore cancellation?
3. How do you run cleanup code when a coroutine is cancelled?
4. What does withTimeoutOrNull return if the block runs out of time?
5. How do you handle an uncaught exception from a launch coroutine?
Coroutines that start, stop, and fail cleanly — that's production-grade async. Next we'll move from single results to streams of values over time with Kotlin Flows.