Coroutines Workbook

Nine short exercises on Kotlin coroutines — suspend functions, suspension points, launch, async and await, coroutineScope, structured concurrency, and dispatchers.

Practice problems for Coroutines: Asynchronous Code That Reads Like It Isn’t. Each takes a minute or two. A couple of exercises auto-check: implement the suspend function and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, on JetBrains’ Kotlin compiler server. The rest stay attempt-then-reveal: fill in the TODO, press Run, then open Show answer to check yourself. Nothing here is a trick question, just direct practice of the ideas from the lesson.

suspend functions

1. Mark a function suspendable

You have fun loadUser(id: Int): User that calls another suspending function. What one keyword does it need?

Try it — edit, then press Run import kotlinx.coroutines.* data class User(val id: Int) suspend fun fetchName(): String { delay(100) return "Ada" } // TODO: add the keyword so loadUser can call fetchName() fun loadUser(id: Int): User { return User(id) } fun main() = runBlocking { println(loadUser(1)) }
Show answer Hide answer
suspend fun loadUser(id: Int): User { /* ... */ }

suspend — it can pause at a suspension point and resume later.

2. Sequential suspends

Implement loadOrders so it calls suspend fun fetchUser(id), then suspend fun fetchOrders(user) in order, and returns the orders.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert import kotlinx.coroutines.* data class User(val id: Int) data class Order(val item: String) suspend fun fetchUser(id: Int): User { delay(100) return User(id) } suspend fun fetchOrders(user: User): List<Order> { delay(100) return listOf(Order("book")) } class Test { @Test fun loadOrders() { val orders = runBlocking { loadOrders(1) } Assert.assertEquals("fetch the user, then their orders", listOf(Order("book")), orders) } } //sampleStart suspend fun loadOrders(id: Int): List<Order> { // TODO: fetch the user, then fetch and return their orders return emptyList() } //sampleEnd
Show answer Hide answer
suspend fun loadOrders(id: Int): List<Order> {
    val user = fetchUser(id)
    return fetchOrders(user)
}

It reads like blocking code, but each call is a suspension point that may release the thread.

3. Call a suspend from a suspend

Write a suspend fun outer() that calls a suspend fun inner() — one of the two places a suspend function may legally be called.

Try it — edit, then press Run import kotlinx.coroutines.* suspend fun inner() { delay(100) println("inner ran") } suspend fun outer() { // TODO: call inner() from here } fun main() = runBlocking { outer() }
Show answer Hide answer
suspend fun inner() { /* ... */ }

suspend fun outer() {
    inner()   // allowed: a suspend function may call another
}

(The other place is inside a coroutine started by launch or async.)

builders

4. Fire and forget

Inside a scope, start a coroutine that loads orders and displays them, without returning a result.

Try it — edit, then press Run import kotlinx.coroutines.* data class Order(val item: String) suspend fun loadOrders(id: Int): List<Order> { delay(100) return listOf(Order("book")) } fun display(orders: List<Order>) = println(orders) fun main() = runBlocking { // 'this' is a CoroutineScope here // TODO: launch a coroutine that loads orders for 42 and displays them }
Show answer Hide answer
scope.launch {
    val orders = loadOrders(42)
    display(orders)
}

5. Run two things concurrently

Inside a coroutineScope, fetch a user and their settings concurrently, then render both.

Try it — edit, then press Run import kotlinx.coroutines.* data class User(val id: Int) data class Settings(val theme: String) suspend fun fetchUser(id: Int): User { delay(100) return User(id) } suspend fun fetchSettings(id: Int): Settings { delay(100) return Settings("dark") } fun render(user: User, settings: Settings) = println("" + user + " " + settings) fun main() = runBlocking { val id = 1 // TODO: fetch user and settings concurrently with async, then render both }
Show answer Hide answer
coroutineScope {
    val user = async { fetchUser(id) }
    val settings = async { fetchSettings(id) }
    render(user.await(), settings.await())
}

The two async blocks overlap; await() suspends until each result is ready.

6. Hold a Deferred, then await it

Inside a coroutine, start fetchUser(id) with async, store the handle, then later get the value out of it.

Try it — edit, then press Run import kotlinx.coroutines.* data class User(val id: Int) suspend fun fetchUser(id: Int): User { delay(100) return User(id) } fun main() = runBlocking { val id = 1 // TODO: start fetchUser(id) with async, hold the Deferred, then await it val user = User(id) // TODO: replace with the awaited async result println(user) }
Show answer Hide answer
val deferred = async { fetchUser(id) }
// ... other work ...
val user = deferred.await()

async returns a Deferred<T>; await() suspends until the value is ready.

structure

7. Children finish before the scope

Inside a coroutineScope, launch two independent jobs, saveA() and saveB(). The block must return only after both finish.

Try it — edit, then press Run import kotlinx.coroutines.* suspend fun saveA() { delay(100) println("A saved") } suspend fun saveB() { delay(100) println("B saved") } fun main() = runBlocking { // TODO: inside a coroutineScope, launch saveA() and saveB() as two children }
Show answer Hide answer
coroutineScope {
    launch { saveA() }
    launch { saveB() }
}
// returns only once both launched children complete

That guarantee is structured concurrency — and if one child fails, the others are cancelled.

8. Run blocking I/O off the main thread

Start a coroutine that performs blocking readFile() on the dispatcher meant for I/O.

Try it — edit, then press Run import kotlinx.coroutines.* fun readFile(): String = "file contents" fun main() = runBlocking { // TODO: launch a coroutine on the I/O dispatcher that calls readFile() }
Show answer Hide answer
scope.launch(Dispatchers.IO) {
    val text = readFile()
}

Dispatchers.IO is for blocking I/O; Dispatchers.Default is for CPU-bound work.

9. Fetch two things, return both

Write a suspend fun loadDashboard(id: Int): Dashboard that fetches fetchUser(id) and fetchSettings(id) concurrently and returns a Dashboard(user, settings).

Try it — edit, then press Run import kotlinx.coroutines.* data class User(val id: Int) data class Settings(val theme: String) data class Dashboard(val user: User, val settings: Settings) suspend fun fetchUser(id: Int): User { delay(100) return User(id) } suspend fun fetchSettings(id: Int): Settings { delay(100) return Settings("dark") } suspend fun loadDashboard(id: Int): Dashboard { // TODO: fetch user and settings concurrently with async, then return both return Dashboard(User(id), Settings("none")) } fun main() = runBlocking { println(loadDashboard(1)) }
Show answer Hide answer
suspend fun loadDashboard(id: Int): Dashboard = coroutineScope {
    val user = async { fetchUser(id) }
    val settings = async { fetchSettings(id) }
    Dashboard(user.await(), settings.await())
}

Sequential-looking code, but the two fetches overlap.


Going deeper: concurrency and structure

12. Overlap the waits

Two suspending fetches each take 100ms. Run them concurrently with async so the total is about 100ms, not 200ms. Fill in both(), then run it and read the elapsed time.

Try it — edit, then press Run import kotlinx.coroutines.* suspend fun fetchA(): String { delay(100); return "A" } suspend fun fetchB(): String { delay(100); return "B" } suspend fun both(): String = coroutineScope { // TODO: start both with async, then await both and concatenate val a = fetchA() val b = fetchB() a + b } fun main() = runBlocking { val start = System.currentTimeMillis() val result = both() val ms = System.currentTimeMillis() - start println(result + " in ~" + ms + "ms") }
Show answer Hide answer
suspend fun both(): String = coroutineScope {
    val a = async { fetchA() }
    val b = async { fetchB() }
    a.await() + b.await()
}

async starts each fetch immediately and returns a Deferred; await() suspends until it’s ready. Because the two 100ms waits overlap, the total is ~100ms instead of ~200ms. The sequential version (calling fetchA() then fetchB() directly) would take ~200ms.

13. What structured concurrency guarantees

Inside a coroutineScope { } you launch two children and one of them throws. What happens to the other child, and where does the exception surface? Reveal to check.

Show answer Hide answer

The sibling is cancelled, and the exception propagates out of the coroutineScope — the scope doesn’t return until all its children finish, and a child failure tears the rest down. That’s the guarantee callbacks can’t give you: no orphaned background work outliving the thing that started it. (This is also why a blanket catch (e: Exception) inside a coroutine is dangerous — it can swallow the CancellationException that cancellation relies on.)


That’s the last workbook in the series. Head back to the lesson, Coroutines, or return to where it all started: In Kotlin, There Are No Primitives.

Comments