← wiki index06 / 10 · The learning path

🌀 The learning path

Kotlin Coroutines

Lightweight threads that make async code read top-to-bottom — and the structured-concurrency rules that keep them from leaking.

7 min read5 key concepts

A coroutine is Kotlin's answer to a problem that predates Kotlin: asynchronous work — a network call, a disk read, a timer — doesn't fit the synchronous, "do this, then this" shape that most code is written in. The old ways to cope were callbacks (nested, error-prone) and threads (heavy, expensive to spin up, dangerous to coordinate). Coroutines are a third way: a block of code that can pause at a suspension point and resume later, without holding a thread hostage while it waits.

The mental model to hold onto: a coroutine is not a thread. It's a lightweight unit of work that runs on a thread, can be moved between threads, and — critically — can suspend, freeing the thread it was on to run something else. You can have thousands of coroutines running on a handful of threads, where thousands of threads would collapse under their own memory and scheduling weight.

suspend functions are the whole trick

The entire coroutine mechanism reduces to one keyword: `suspend`. A suspend function is an ordinary function that happens to be allowed to pause. When it pauses, it doesn't block the thread — it yields control back so the thread can run other coroutines, and it carries a hidden continuation that remembers where it paused and what its local variables were.

  • A suspend function can only be called from another suspend function or from a coroutine builder — the compiler enforces this, which is why async code can't silently infect synchronous code
  • `launch` fires a coroutine and forgets it (fire-and-forget); `async` fires one and hands back a `Deferred` you can `await` to get a result
  • `withContext(Dispatchers.IO)` switches the thread a block runs on and switches back when it returns — this is how you move slow work off the main thread
  • A suspend function that does blocking work is still blocking its thread — suspending only helps when the thing you're waiting on cooperates

Structured concurrency: the tree that owns your work

The single most important idea in coroutines is that work is organised as a tree. Every coroutine has a parent, and the parent's scope owns the lifetime of its children. When a scope is cancelled, every coroutine in it is cancelled; when a child throws, its siblings and parent learn about it. There is no such thing as a fire-and-forget coroutine that leaks past the thing that created it — that's the point.

  • Cancellation is cooperative: a coroutine checks a cancellation flag at every suspension point. A coroutine stuck in a tight non-suspending loop will not be cancelled until it suspends — which is why long CPU loops must call `yield()` or `ensureActive()`
  • `coroutineScope` fails fast: if one child throws, the whole scope cancels and the exception propagates. `supervisorScope` isolates children — one failing child doesn't take down its siblings
  • A `Job` is the handle to a coroutine's lifecycle; a `CoroutineContext` is the bundle of `Job` + `Dispatcher` (and any other elements) every coroutine carries
  • Exception handling is deliberate: an uncaught exception in a root `launch` goes to a `CoroutineExceptionHandler` (or crashes the app); inside `async`, it surfaces at `await()` instead

Dispatchers and scopes: where, and for how long

A `Dispatcher` decides which thread a coroutine runs on; a `Scope` decides when the whole tree of coroutines gets torn down. Mixing the two up is where most coroutine bugs come from.

  • `Dispatchers.Main` — the UI thread, where you touch views and where state that drives the UI must be mutated
  • `Dispatchers.IO` — a shared pool tuned for blocking I/O (network, disk); use it for anything that waits on the outside world
  • `Dispatchers.Default` — CPU-bound work, sized to the number of cores
  • `viewModelScope` / `lifecycleScope` — scopes tied to a screen's lifetime. The ViewModel is destroyed on rotation or when the screen closes, and the scope auto-cancels every coroutine it launched — this is what stops a network call from updating a dead screen
  • `GlobalScope` is the escape hatch that leaks work past its owner — it exists, and you almost never want it

In this note

suspendlaunch vs asyncdispatchersstructured concurrencycooperative cancellation

Prefer it hands-on?

This note has a matching interactive topic with diagrams and a runnable repo.