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
Prefer it hands-on?
This note has a matching interactive topic with diagrams and a runnable repo.