Kotlin Coroutines
Suspend functions, launch and async, structured concurrency, and a first look at Flow.
Why coroutines?
A traditional blocked thread (waiting on a network call, a database query) sits idle but still ties up an entire OS thread — an expensive resource, since threads are limited (typically a few thousand per process before things degrade badly). Coroutines let you write code that looks sequential and blocking, but can suspend at an await point without blocking the underlying thread, which is then free to run other work in the meantime. Thousands of coroutines can run on a small pool of just a few real threads.
suspend functions
A function marked suspend can pause its execution at certain points (usually while waiting on another suspend function) without blocking the thread it's running on:
import kotlinx.coroutines.*
suspend fun fetchUserName(): String {
delay(1000) // suspends for 1 second — does NOT block the underlying thread
return "Ada"
}
suspend fun main() {
println("Fetching...")
val name = fetchUserName() // suspends here until the result is ready
println("Got: $name")
}
delay() is coroutines' suspending equivalent of Thread.sleep() — but where Thread.sleep() blocks an entire OS thread for its duration, delay() merely suspends the coroutine, freeing the thread to do other useful work until it resumes.
A suspend function can only be called from another suspend function, or from inside a coroutine builder like launch or runBlocking — this is enforced by the compiler, so you can never accidentally call suspending code from an ordinary function and get confusing behavior.
launch and async
launch and async are coroutine builders — they start a new coroutine, but differ in what they return.
launch — starts a coroutine and returns a Job, used for "fire and forget" work where you don't need a result back:
import kotlinx.coroutines.*
fun main() = runBlocking { // runBlocking bridges regular blocking code into the coroutine world
launch {
delay(500)
println("Task from launch")
}
println("Task from main")
}
// Task from main
// Task from launch (printed ~500ms later)
async — starts a coroutine and returns a Deferred<T>, used when you need a result back, retrieved with .await():
import kotlinx.coroutines.*
suspend fun fetchPrice(item: String): Int {
delay(300)
return item.length * 10
}
fun main() = runBlocking {
val priceA = async { fetchPrice("Laptop") } // starts running immediately, concurrently
val priceB = async { fetchPrice("Mouse") } // also starts immediately
val total = priceA.await() + priceB.await() // suspends until BOTH are done
println("Total: $total") // both fetches ran concurrently, not one after another
}
Because both async blocks start running immediately (not when .await() is called), the two 300ms delays overlap — the whole thing takes roughly 300ms total, not 600ms, exactly the same concurrency benefit you'd get from Go's goroutines or Java's ExecutorService, but expressed as regular-looking sequential code.
Structured concurrency
Kotlin's coroutines are structured: every coroutine is launched inside a CoroutineScope, and a scope won't complete until all the coroutines launched inside it have finished. This prevents "leaked" background work that outlives the part of the program that started it — a real problem with raw threads, which have no built-in parent/child relationship.
import kotlinx.coroutines.*
suspend fun processOrder() = coroutineScope { // creates a new scope tied to this function
launch {
delay(200)
println("Sent confirmation email")
}
launch {
delay(100)
println("Updated inventory")
}
println("Order processing started")
} // this function does NOT return until BOTH launched coroutines above have completed
If a child coroutine throws an exception, structured concurrency ensures it propagates up to the scope and cancels sibling coroutines too, rather than failing silently in the background — the same predictable error-handling story you'd expect from ordinary sequential code.
A brief look at Flow
Flow represents an asynchronous stream of multiple values over time (as opposed to suspend, which produces a single value once) — conceptually similar to RxJava's Observable or Go's channels, but built entirely on coroutines.
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
fun countDown(): Flow<Int> = flow {
for (i in 3 downTo 1) {
delay(500)
emit(i) // produces one value into the stream
}
}
fun main() = runBlocking {
countDown().collect { value -> // collect suspends and receives each emitted value
println(value)
}
}
// 3
// 2
// 1
Flow is the standard tool for modeling things like a live search-as-you-type text field, incoming WebSocket messages, or repeated database query results — anywhere you need "zero or more values over time" rather than a single asynchronous result.
Common mistakes
- Calling a blocking function (like
Thread.sleep()or a blocking network call) from inside a coroutine instead of its suspending equivalent (delay(), a suspending HTTP client) — this defeats the whole point, since it blocks the real underlying thread anyway. - Using
launchwhen you actually need the result back — reach forasync/.await()instead. - Forgetting that
async { ... }starts running immediately, not when.await()is called — the concurrency happens because both are already in flight before you ever call.await()on either.
Interview questions
Q: What's the practical difference between a coroutine and a thread? A thread is an OS-level construct — expensive to create, limited in how many can exist at once, and always blocks the thread it runs on while waiting. A coroutine is a much lighter-weight, language/runtime-level construct — thousands can run on a small pool of just a few real threads, because a suspended coroutine simply steps aside and lets that thread run other work instead of blocking it.
Q: What's the difference between launch and async?
launch starts a coroutine and returns a Job, intended for work where you don't need a return value ("fire and forget"). async starts a coroutine and returns a Deferred<T>, intended for work where you do need a result, retrieved later by calling .await() — which suspends until that specific coroutine finishes.