Many tasks involve waiting: downloading a file, querying a database, calling a web API. If a program simply stops and waits, an Android app freezes and a server can't handle other requests in the meantime. The traditional answers, threads and callbacks, work but get complicated quickly, with code scattered across nested callback functions.
Coroutines are Kotlin's answer. A coroutine is a lightweight task that can suspend, pausing while it waits without blocking a thread, and resume later exactly where it left off. You write asynchronous code top to bottom, like ordinary code, and Kotlin handles the waiting. Thousands of coroutines can run on just a few threads.
In this lesson you'll write suspend functions, start coroutines with launch and async, run work in parallel and collect the results, move work to background threads with dispatchers, and cancel or time out work safely. Coroutines live in the kotlinx.coroutines library, which you add to your project's build file; Android projects usually include it already.
suspend functions
A suspend function can pause without blocking the thread it's running on:
suspend fun fetchUser(id: Int): String {
delay(1000) // pretend network call: suspends for one second
return "User $id"
}delaysuspends the coroutine;Thread.sleepwould block the whole thread, so avoid it in coroutines.- A suspend function can only be called from another suspend function or from inside a coroutine.
- Calling it looks completely normal:
val user = fetchUser(7). No callbacks are needed.
Starting coroutines: runBlocking, launch and async
runBlocking { }starts a coroutine and blocks until everything inside finishes. It bridges normal code to coroutines, mainly inmainfunctions and tests.launch { }starts a coroutine that does work and returns aJob, which you canjoin()(wait for) orcancel(). Use it for "fire and forget" tasks.async { }starts a coroutine that produces a result, returning aDeferred<T>. Callawait()to get the value.
Starting two async blocks before awaiting either one makes them run at the same time, so two one-second calls take about one second in total instead of two.
Structured concurrency and dispatchers
Coroutines are started inside a scope, and a scope doesn't finish until all its children finish. If one child fails, the others are cancelled. This structured concurrency means coroutines can't be forgotten and leak. coroutineScope { } creates a scope inside a suspend function.
Dispatchers decide which threads run a coroutine:
| Dispatcher | For |
|---|---|
Dispatchers.Main | Updating the UI (Android) |
Dispatchers.IO | Network and disk work |
Dispatchers.Default | CPU-heavy calculations |
withContext(Dispatchers.IO) { ... } switches to another dispatcher for a block and returns its result, then continues where it was.
Cancellation and timeouts
Coroutines can be cancelled: job.cancel(), or automatically when their scope ends, such as when an Android screen closes. Library suspend functions like delay check for cancellation and stop promptly.
withTimeout(2000) { ... } cancels the block if it takes longer than two seconds and throws a TimeoutCancellationException; withTimeoutOrNull returns null instead, which is often easier to handle. Exceptions inside launch propagate to the parent scope, while exceptions inside async are thrown when you call await(), so wrap that in try/catch.
Kotlin Coroutines Syntax
Coroutine basics
import kotlinx.coroutines.*
suspend fun load(): String { delay(500); return "data" }
fun main() = runBlocking {
val job = launch { load() }
val a = async { load() }
val b = async { load() }
println(a.await() + b.await())
job.join()
val r = withContext(Dispatchers.IO) { load() }
val maybe = withTimeoutOrNull(1000) { load() }
}Kotlin Coroutines Summary
- Coroutines are lightweight tasks that suspend while waiting instead of blocking threads.
- suspend functions can pause; call them from coroutines or other suspend functions.
- runBlocking bridges to normal code, launch starts a job, async returns a result via await.
- Structured concurrency: scopes wait for their children and cancel them on failure.
- Dispatchers choose threads; withContext switches; withTimeoutOrNull limits how long work takes.
Kotlin Coroutines Examples
Each example shows the code, the output it produces and what each part does.
launch and delay
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(200)
println("World!")
}
launch {
delay(100)
println("Kotlin")
}
println("Hello")
}Hello
Kotlin
World!Line-by-line explanation
- Line 3runBlocking starts a coroutine and waits for everything inside it to finish.
- Lines 4 to 11Two coroutines start and suspend at their delays, so main's println runs first.
- Lines 8 to 10The shorter delay finishes first, then the longer one.
Parallel work with async
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
suspend fun fetchPrice(item: String): Double {
delay(1000)
return if (item == "tea") 2.5 else 3.75
}
fun main() = runBlocking {
val time = measureTimeMillis {
val tea = async { fetchPrice("tea") }
val cake = async { fetchPrice("cake") }
println("Total: ${tea.await() + cake.await()}")
}
println("Finished in under 1.5 seconds: ${time < 1500}")
}Total: 6.25
Finished in under 1.5 seconds: trueLine-by-line explanation
- Lines 4 to 7A suspend function simulating a one-second network call.
- Lines 11 to 12Both async calls start immediately and run at the same time.
- Line 13await suspends until each result is ready.
- Line 15The two one-second calls overlap, so the total is about one second, not two.
withContext, timeouts and cancellation
import kotlinx.coroutines.*
suspend fun slowReport(): String = withContext(Dispatchers.Default) {
delay(3000)
"Report ready"
}
fun main() = runBlocking {
val result = withTimeoutOrNull(1000) { slowReport() }
println(result ?: "Timed out, showing cached report")
val job = launch {
repeat(10) { i ->
println("Working $i")
delay(300)
}
}
delay(700)
job.cancel()
println("Cancelled: ${job.isCancelled}")
}Timed out, showing cached report
Working 0
Working 1
Working 2
Cancelled: trueLine-by-line explanation
- Lines 3 to 6withContext runs the work on the Default dispatcher and returns its result.
- Line 9The report takes 3 seconds, so the 1 second timeout gives null.
- Lines 12 to 17A job that prints every 300 ms.
- Lines 18 to 19After 700 ms, three iterations have printed; cancel stops the job at its next delay.
Common Kotlin Coroutines Mistakes
Calling a suspend function from normal code
fun main() {
val user = fetchUser(7) // error: suspend function should be called only from a coroutine
}fun main() = runBlocking {
val user = fetchUser(7)
}Using Thread.sleep in a coroutine
launch { Thread.sleep(1000); println("done") } // blocks the thread for everyonelaunch { delay(1000); println("done") } // suspends only this coroutineAwaiting immediately, one at a time
val a = async { load("a") }.await()
val b = async { load("b") }.await() // runs after a: no parallelismval a = async { load("a") }
val b = async { load("b") }
println(a.await() + b.await()) // both run at onceKotlin Coroutines in Real Projects
Coroutines in Android and servers
Android's architecture components are built around coroutines: viewModelScope.launch { } starts work tied to a screen's lifetime and cancels it automatically when the user leaves, and libraries like Room and Retrofit provide suspend functions for database and network calls. Server frameworks such as Ktor handle every request in a coroutine, serving many thousands of concurrent connections on a small number of threads.
Kotlin Coroutines FAQ
Are Kotlin coroutines the same as threads?
No. A coroutine is a much lighter unit of work that runs on a thread but can suspend and let another coroutine use that thread meanwhile. A program can run 100,000 coroutines that each delay for a moment without trouble, whereas 100,000 threads would use a huge amount of memory. Dispatchers decide which threads your coroutines share.
Why doesn't my coroutine stop when I cancel it?
Cancellation is cooperative: a coroutine only stops at a point that checks for it, such as delay, yield or another suspending library call. A tight loop doing pure calculation never checks, so it keeps running. Call ensureActive() or check isActive inside long loops so cancellation can take effect.
Why should I avoid GlobalScope in Kotlin?
Coroutines started in GlobalScope aren't tied to anything, so nothing cancels them when the screen or request that started them goes away, and their failures aren't reported to a parent. That's the kind of leak structured concurrency exists to prevent. Use a scope with a clear lifetime instead, such as viewModelScope on Android or coroutineScope { } inside a suspend function.
How do I add coroutines to a Kotlin project?
Coroutines live in the kotlinx.coroutines library, not the standard library, so add the org.jetbrains.kotlinx:kotlinx-coroutines-core dependency to your Gradle build. Android projects usually add kotlinx-coroutines-android as well, which provides Dispatchers.Main. The suspend keyword itself is part of the language and needs no dependency.
What happens when one coroutine in a scope fails?
In a normal scope such as coroutineScope { }, a failure in one child cancels its siblings and the exception is rethrown from the scope, so you can catch it around the whole block. If the children should be independent, so one failure doesn't stop the others, use supervisorScope { } and handle each child's errors separately.
Kotlin Coroutines Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the declaration so the function can call delay.
fun loadData(): String {
delay(500)
return "data"
}
Which builder returns a result you can await?
What does this print?
fun main() = runBlocking {
launch { delay(50); print("B") }
print("A")
}
Which dispatcher suits reading a large file?
Two async calls each take one second. When does awaiting both take about one second in total?
Kotlin Coroutines Quiz
5 questions to check what you learned.
What does
delay(1000)do inside a coroutine?- Blocks the thread for a second
- Suspends the coroutine for a second without blocking the thread
- Cancels the coroutine
- Nothing
What is structured concurrency?
- Running coroutines in alphabetical order
- Using only one coroutine
- Scopes wait for their child coroutines and cancel them if one fails, so none are leaked
- A type of dispatcher
What does
withTimeoutOrNull(500) { ... }return if the block takes too long?- An exception
- The partial result
- null
- 500
What does
withContext(Dispatchers.IO) { }do?- Starts a new program
- Runs the block on the IO dispatcher and returns its result
- Cancels the coroutine
- Creates a new scope that never ends
Why is runBlocking rarely used inside Android apps?
- It blocks the current thread, which would freeze the UI on the main thread
- It's deprecated
- It can't call suspend functions
- It only works on servers