Kotlin Coroutines

Write asynchronous code that reads like normal code with coroutines: suspend functions, runBlocking, launch, async and await, delay, structured concurrency, dispatchers and timeouts.

Lesson 22 of 43Advanced17 min read

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:

Kotlin
suspend fun fetchUser(id: Int): String {
    delay(1000)              // pretend network call: suspends for one second
    return "User $id"
}
  • delay suspends the coroutine; Thread.sleep would 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 in main functions and tests.
  • launch { } starts a coroutine that does work and returns a Job, which you can join() (wait for) or cancel(). Use it for "fire and forget" tasks.
  • async { } starts a coroutine that produces a result, returning a Deferred<T>. Call await() 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:

DispatcherFor
Dispatchers.MainUpdating the UI (Android)
Dispatchers.IONetwork and disk work
Dispatchers.DefaultCPU-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.

Example 1

launch and delay

Kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(200)
        println("World!")
    }
    launch {
        delay(100)
        println("Kotlin")
    }
    println("Hello")
}
Output
Hello
Kotlin
World!
Line-by-line explanation
  1. Line 3runBlocking starts a coroutine and waits for everything inside it to finish.
  2. Lines 4 to 11Two coroutines start and suspend at their delays, so main's println runs first.
  3. Lines 8 to 10The shorter delay finishes first, then the longer one.
Example 2

Parallel work with async

Kotlin
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}")
}
Output
Total: 6.25
Finished in under 1.5 seconds: true
Line-by-line explanation
  1. Lines 4 to 7A suspend function simulating a one-second network call.
  2. Lines 11 to 12Both async calls start immediately and run at the same time.
  3. Line 13await suspends until each result is ready.
  4. Line 15The two one-second calls overlap, so the total is about one second, not two.
Example 3

withContext, timeouts and cancellation

Kotlin
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}")
}
Output
Timed out, showing cached report
Working 0
Working 1
Working 2
Cancelled: true
Line-by-line explanation
  1. Lines 3 to 6withContext runs the work on the Default dispatcher and returns its result.
  2. Line 9The report takes 3 seconds, so the 1 second timeout gives null.
  3. Lines 12 to 17A job that prints every 300 ms.
  4. 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

Don't
fun main() {
    val user = fetchUser(7)      // error: suspend function should be called only from a coroutine
}
Do
fun main() = runBlocking {
    val user = fetchUser(7)
}

Using Thread.sleep in a coroutine

Don't
launch { Thread.sleep(1000); println("done") }   // blocks the thread for everyone
Do
launch { delay(1000); println("done") }          // suspends only this coroutine

Awaiting immediately, one at a time

Don't
val a = async { load("a") }.await()
val b = async { load("b") }.await()    // runs after a: no parallelism
Do
val a = async { load("a") }
val b = async { load("b") }
println(a.await() + b.await())         // both run at once

Kotlin 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.

Fill in the blankseasy

Complete the declaration so the function can call delay.

 fun loadData(): String {
    delay(500)
    return "data"
}
Multiple choiceeasy

Which builder returns a result you can await?

Predict the outputmedium

What does this print?

fun main() = runBlocking {
    launch { delay(50); print("B") }
    print("A")
}
Multiple choicemedium

Which dispatcher suits reading a large file?

Multiple choicehard

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.

  1. 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
  2. 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
  3. What does withTimeoutOrNull(500) { ... } return if the block takes too long?

    • An exception
    • The partial result
    • null
    • 500
  4. 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
  5. 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