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?
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.
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.
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.
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.
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.
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.
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 completeThat 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.
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).
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.
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