Coroutines
Async programming without callback hell. Learn suspend functions, launch, async/await, and Flow for reactive streams.
By the end of this chapter, you will:
- Understand why coroutines exist and how they differ from threads
- Launch coroutines with
launchandasync - Write and call
suspendfunctions - Handle cancellation and exceptions
- Understand the basics of Flow
The Problem with Threads
Traditional threading is expensive. Each thread consumes ~1MB of memory, and context switching is slow. You cannot launch millions of threads.
import kotlin.concurrent.thread
// Bad: Creating a thread for every network request
fun fetchUserBad(userId: String) {
thread { // Expensive! Each thread = ~1MB memory
val user = makeNetworkRequest(userId)
println(user)
}
}
// Good: Coroutine — lightweight, managed by Kotlin's runtime
suspend fun fetchUserGood(userId: String) {
val user = makeNetworkRequest(userId) // suspends, doesn't block
println(user)
}
Your First Coroutine
To launch a coroutine, you need a CoroutineScope. For learning, use runBlocking. In real apps, use structured scopes tied to lifecycle (ViewModelScope, LifecycleScope).
GlobalScope lives for the entire application lifetime and is not bound to any lifecycle. Prefer structured scopes like lifecycleScope, viewModelScope, or create your own CoroutineScope tied to a specific component.
import kotlinx.coroutines.*
fun main() = runBlocking { // Blocks main thread until coroutines complete
launch { // Launches a new coroutine
delay(1000L) // Non-blocking delay (like setTimeout)
println("World!")
}
println("Hello,") // Prints immediately
// Output: Hello, (wait 1s) World!
}
launch vs async
runBlocking {
// launch: fire and forget, returns Job
val job = launch {
delay(100)
println("Done")
}
job.join() // Wait for completion
// async: returns a result, returns Deferred<T>
val deferred = async {
delay(100)
"Result"
}
val result = deferred.await() // "Result"
}
Compare with JS / Python
// Promise-based
async function main() {
setTimeout(() => console.log("World!"), 1000);
console.log("Hello,");
}
// Or with Promise
const result = await fetch(url);
const data = await result.json();
import asyncio
async def main():
await asyncio.sleep(1)
print("World!")
asyncio.run(main())
Kotlin difference: Coroutines are more structured than JS Promises. launch/async run in a scope, and child coroutines are cancelled when the parent is. This prevents leaks. JS and Python have no built-in structured concurrency.
Dispatchers
Dispatchers determine which thread or thread pool a coroutine runs on. Choosing the right dispatcher prevents blocking the UI thread and keeps your app responsive.
// Dispatchers.Main — Android/UI thread (updates views)
// Dispatchers.IO — Network and disk I/O (thread pool)
// Dispatchers.Default — CPU-intensive work (thread pool)
// Dispatchers.Unconfined — Starts in caller context, jumps around
suspend fun loadData() {
withContext(Dispatchers.IO) {
val data = fetchFromNetwork() // Safe to block with IO
}
}
Main, network/files on IO, algorithms/JSON parsing on Default. Never call Dispatchers.IO functions directly from the UI thread without withContext.
Switching Contexts with withContext
withContext lets you move a coroutine to a different dispatcher, do work, and return the result — all without callbacks. Execution is sequential but happens on different threads.
suspend fun updateDashboard() {
val data = withContext(Dispatchers.IO) {
fetchFromNetwork() // runs on IO thread pool
}
val processed = withContext(Dispatchers.Default) {
parseAndTransform(data) // CPU work on Default pool
}
withContext(Dispatchers.Main) {
displayResult(processed) // back to UI thread
}
}
IO, parse it on Default, then render it on Main. Each step is readable and sequential, yet runs on the optimal thread.
SupervisorScope
In a regular coroutineScope, if one child fails, all siblings are cancelled. supervisorScope isolates failures so that one crashing child doesn't bring down the others.
supervisorScope {
val job1 = launch {
mightFail() // throws exception
}
val job2 = launch {
alwaysWorks() // continues running even if job1 fails
}
// Optionally handle exceptions per child
job1.invokeOnCompletion { exception ->
exception?.let { println("job1 failed: $it") }
}
}
SupervisorJob to keep a ViewModel alive when one use case crashes.
Async Exception Handling
Exceptions in async are deferred until you call await(). Handle them with try/catch or a CoroutineExceptionHandler for uncaught exceptions.
// Exception is deferred until await()
val deferred = async {
fetchDataThatMightFail()
}
try {
val result = deferred.await()
} catch (e: Exception) {
println("Handled: $e")
}
// Global uncaught exception handler
val handler = CoroutineExceptionHandler { _, exception ->
println("Uncaught: $exception")
}
val scope = CoroutineScope(Dispatchers.Main + handler)
scope.launch {
throw RuntimeException("Oops")
}
await() in try/catch when you expect failures. Use CoroutineExceptionHandler as a safety net for unexpected crashes in top-level scopes.
Channels
Channels provide a way for coroutines to communicate with each other — like Go channels or Python queues. They're perfect for producer-consumer patterns where one coroutine produces data and another consumes it.
import kotlinx.coroutines.channels.Channel
val channel = Channel<Int>()
// Producer
launch {
for (x in 1..5) channel.send(x * x)
channel.close()
}
// Consumer
launch {
for (value in channel) {
println(value) // 1, 4, 9, 16, 25
}
}
Suspend Functions
A suspend function can pause execution without blocking the thread. It can only be called from another suspend function or a coroutine.
// Mark a function as suspend
suspend fun fetchUser(id: String): User {
delay(1000) // Suspend for 1 second, but don't block the thread
return User(id, "Alex")
}
suspend fun fetchOrders(userId: String): List<Order> {
delay(500)
return listOf(Order("1"), Order("2"))
}
// Sequential execution (default)
suspend fun loadDashboard(userId: String) {
val user = fetchUser(userId) // Wait for user
val orders = fetchOrders(user.id) // Then fetch orders
println("Loaded $user with $orders")
}
// Concurrent execution with async
suspend fun loadDashboardFast(userId: String) {
coroutineScope { // Creates a scope for structured concurrency
val userDeferred = async { fetchUser(userId) }
val ordersDeferred = async { fetchOrders(userId) }
val user = userDeferred.await() // Both started concurrently!
val orders = ordersDeferred.await() // Wait for both
println("Loaded $user with $orders")
}
}
• Suspend functions look synchronous but execute asynchronously
• Sequential by default, concurrent when you explicitly use
async• Differs from JavaScript where
async functions always return Promises and everything runs concurrently unless you await
async only when requests are independent (fetch profile + fetch notifications simultaneously).
Cancellation
Coroutines are cooperative. They check for cancellation at suspension points. Use isActive for long-running CPU work.
val job = launch {
repeat(1000) { i ->
println("Working $i...")
delay(500)
}
}
delay(1300) // Let it print a few times
job.cancel() // Cancel the coroutine
job.join() // Wait for cancellation to complete
// For CPU-intensive work, check isActive
launch {
while (isActive) {
doSomeWork()
}
}
viewModelScope automatically cancels all children when the ViewModel is cleared — preventing leaks and wasted bandwidth.
Flow (Reactive Streams)
Flow is a cold stream of values — like RxJava or JS Observables, but built into coroutines.
import kotlinx.coroutines.flow.*
// A Flow emits multiple values over time
fun userUpdates(): Flow<User> = flow {
emit(User("1", "Alex"))
delay(1000)
emit(User("1", "Alex Updated"))
}
// Collecting a Flow
runBlocking {
userUpdates().collect { user ->
println("Received: $user")
}
}
// Flow operators (like collections!)
userUpdates()
.filter { it.name.startsWith("A") }
.map { it.name.uppercase() }
.collect { println(it) }
repeatOnLifecycle, works with any coroutine scope, and supports backpressure and complex transformations.
Flow to automatically emit new values when data changes. Combine multiple flows for form validation (e.g., email.combine(password) { ... }). Use StateFlow for UI state that survives configuration changes.
// SupervisorJob — child failures don't cancel siblings
val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor + Dispatchers.Default)
scope.launch {
println("Task A running")
}
scope.launch {
throw RuntimeException("Task B failed") // Task A keeps running!
}
// withTimeoutOrNull — return null on timeout
val result = withTimeoutOrNull(1000L) {
fetchSlowNetworkData() // if this takes > 1s, returns null
}
📖 Official docs: Coroutines Overview (kotlinlang.org) · Flow (kotlinlang.org)
Error Handling with Result
Kotlin's Result<T> type encapsulates success or failure without throwing exceptions. Use runCatching { } to execute code that might throw, then handle both cases functionally.
// runCatching returns Result
val result: Result<User> = runCatching {
fetchUserFromNetwork("123") // might throw IOException
}
// Handle with fold
result.fold(
onSuccess = { user -> displayUser(user) },
onFailure = { error -> showError(error.message) }
)
// Or use functional chaining
val name = result
.map { it.name.uppercase() } // transform on success
.recover { "Unknown User" } // provide fallback on failure
.getOrDefault("Guest") // unwrap with default
// In coroutines — use suspendCatching (or runCatching works too)
suspend fun loadData(): Result<Data> = runCatching {
val response = api.fetchData()
response.validate() // might throw
response.toData()
}
// Collecting Result from async
val deferred = async { loadData() }
deferred.await() // Result
Result for expected failures (network errors, validation failures) where the caller should decide what to do. Use exceptions for programming errors (null dereference, illegal state) that should never happen in production.
Try It Yourself
Write a suspend function that fetches two URLs concurrently and returns the combined string length of both responses.
suspend fun fetchBoth(url1: String, url2: String): Int {
// Your code here:
}
// Should work like:
// val total = fetchBoth("https://api1.com", "https://api2.com")
// println(total) // sum of both response lengths
Stuck? Show hint
coroutineScope { }, launch two async blocks, await() both, then sum their .length.
Concept Check
What happens if you call delay(1000) inside a regular (non-suspend) function?