Coroutines Basics
Learn what problem coroutines solve, suspend functions, launch and async builders, runBlocking, and Dispatchers.
Introduction
Coroutines are Kotlin's answer to writing asynchronous, non-blocking code without the deeply nested callbacks that plague callback-based async code, and without the heavy resource cost of spawning a full OS thread per task. They became stable in Kotlin 1.3 (2018) and are now the standard way to handle concurrency in Kotlin, from Android apps to backend services.
This lesson introduces the core building blocks: `suspend` functions, the `launch` and `async` coroutine builders, `runBlocking`, structured concurrency, and `Dispatchers` for choosing where coroutines run.
The Problem Coroutines Solve
Imagine a function that needs to fetch data from a network call, then a database, then process the results — each step takes real time and shouldn't block the thread it runs on while waiting. Traditionally, you either block a thread (wasteful, and threads are expensive — typically limited to a few thousand per process) or use callbacks (which nest deeply and become hard to read and reason about, sometimes called "callback hell"). Coroutines let you write this code sequentially, top to bottom, while the compiler transforms it under the hood into non-blocking code that suspends and resumes without tying up a full thread while waiting.
suspend Functions
A function marked `suspend` can pause its execution at certain points (typically while waiting on I/O) without blocking the underlying thread, then resume later exactly where it left off. A `suspend` function can only be called from another `suspend` function, or from inside a coroutine — this restriction is enforced by the compiler, so you always know exactly where suspension is possible.
import kotlinx.coroutines.delay
suspend fun fetchUserName(): String { delay(1000) // simulates a non-blocking network call return "Priya"}`delay()` is coroutines' non-blocking equivalent of `Thread.sleep()` — it suspends the coroutine without blocking the thread it was running on, freeing that thread to do other work in the meantime.
Getting Set Up
Coroutines are not part of the Kotlin standard library itself — they require the `kotlinx-coroutines-core` library, maintained by JetBrains. In a Gradle project, add it as a dependency.
// build.gradle.ktsdependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")}runBlocking and launch
`runBlocking` starts a coroutine and blocks the current thread until it completes — it exists mainly to bridge regular blocking code (like a `main` function) into the coroutine world, and is commonly used in examples and tests. `launch` starts a new coroutine that runs concurrently and does not return a result.
import kotlinx.coroutines.*
fun main() = runBlocking { println("Start")
launch { delay(1000) println("Coroutine finished after 1 second") }
println("End of main (coroutine still running in the background)")}Click Run to see what this code prints.
Notice the order: `launch` starts the coroutine and immediately continues running the rest of `main`, without waiting. The delayed message prints last, once its one-second delay elapses — this is exactly the non-blocking behavior coroutines are designed to provide.
async and await
Unlike `launch`, `async` starts a coroutine that produces a result, returned as a `Deferred<T>`. Calling `.await()` on it suspends until that result is ready. This is the standard pattern for running multiple independent operations concurrently and then combining their results.
import kotlinx.coroutines.*
suspend fun fetchScore(): Int { delay(500) return 42}
suspend fun fetchBonus(): Int { delay(300) return 8}
fun main() = runBlocking { val score = async { fetchScore() } val bonus = async { fetchBonus() }
println("Total: ${score.await() + bonus.await()}")}Click Run to see what this code prints.
Because both `async` calls start immediately and run concurrently, the total time here is roughly 500ms (the longer of the two), not 800ms (their sum) — a sequential pair of plain `suspend fun` calls without `async` would have taken the full 800ms instead.
Structured Concurrency
Kotlin coroutines follow a principle called structured concurrency: every coroutine runs inside a `CoroutineScope`, and a parent scope will not complete until all the coroutines launched inside it have finished. This prevents coroutines from silently leaking or outliving the context that started them — if the parent scope is cancelled, every coroutine launched inside it is automatically cancelled too.
Dispatchers
A `Dispatcher` determines which thread (or thread pool) a coroutine runs on. You choose a dispatcher explicitly rather than managing threads by hand.
| Dispatcher | Best For |
|---|---|
| Dispatchers.Main | UI-thread work in Android/desktop UI apps. |
| Dispatchers.IO | Network calls, file I/O, and database access — a large thread pool tuned for blocking I/O work. |
| Dispatchers.Default | CPU-intensive work like sorting large lists or complex calculations. |
| Dispatchers.Unconfined | Advanced/rare cases; runs in the caller's thread without confinement — avoid unless you specifically need it. |
import kotlinx.coroutines.*
suspend fun loadFromNetwork(): String = withContext(Dispatchers.IO) { delay(500) "Downloaded data"}
fun main() = runBlocking { println(loadFromNetwork())}Click Run to see what this code prints.
Coroutines vs Threads
A coroutine is far lighter weight than an OS thread — you can comfortably run tens of thousands of coroutines concurrently, where a similar number of raw threads would exhaust system resources. Multiple coroutines can share and take turns on the same small pool of underlying threads, since a suspended coroutine releases its thread instead of blocking it.
Common Mistakes
- Calling a `suspend` function from ordinary non-suspend code — the compiler will reject it; you need a coroutine builder or another suspend function.
- Using `runBlocking` inside production code paths (e.g. inside an Android UI thread) — it defeats the purpose of coroutines by blocking the calling thread; it is meant mainly for `main` functions and tests.
- Launching independent `async` calls sequentially with `.await()` called immediately after each one — that removes the concurrency benefit; start all of them first, then await each.
- Forgetting that `delay()` and `Thread.sleep()` are not interchangeable — `delay()` suspends without blocking the thread; `Thread.sleep()` blocks it entirely, even inside a coroutine.
Best Practices
- Mark any function that performs suspendable work (network calls, delays, database access) as `suspend`, so the compiler enforces it is only called safely.
- Use `Dispatchers.IO` for blocking I/O work and `Dispatchers.Default` for CPU-heavy work — don't run either on `Dispatchers.Main`.
- Start independent `async` operations first, then call `.await()` on each afterward, to get real concurrency instead of accidental sequencing.
- Let structured concurrency manage cancellation for you — avoid manually tracking or cancelling individual coroutines outside of a proper scope when you don't have to.
Frequently Asked Questions
No. Coroutines are a much lighter-weight abstraction that can run on top of a small pool of real threads, suspending and resuming without permanently occupying a thread while waiting.
Not necessarily — plain sequential code is fine for simple, fast operations. Coroutines earn their complexity once you have genuinely asynchronous work: network calls, file I/O, or many independent tasks that can run concurrently.
Yes. Coroutines are a general Kotlin language feature (backed by the kotlinx-coroutines-core library) and are widely used in backend frameworks like Ktor and Spring, not just Android.
Key Takeaways
- Coroutines let you write asynchronous code sequentially, avoiding deeply nested callbacks and expensive OS threads.
- `suspend` functions can pause and resume without blocking a thread, and can only be called from a coroutine or another suspend function.
- `launch` starts a fire-and-forget coroutine; `async`/`await` starts one that returns a result you can wait on.
- Structured concurrency ties every coroutine to a scope, so parent scopes wait for (and can cancel) all their children automatically.
- Dispatchers (Main, IO, Default) control which thread pool a coroutine actually runs on.
Summary
You now understand the core building blocks of Kotlin coroutines — suspend functions, launch, async/await, and dispatchers. Next, in the final lesson of this course, you'll wrap up with idiomatic Kotlin best practices and where to go from here.