Kotlin Coroutines Cheat Sheet
Covers launching coroutines, suspend functions, coroutine builders, structured concurrency with scopes, and Flow for asynchronous Kotlin code.
Coroutine Builders
launch and async, the two primary ways to start a coroutine.
import kotlinx.coroutines.*fun main() = runBlocking { // launch: fire-and-forget coroutine, returns a Job val job = launch { delay(1000L) println("World!") } println("Hello,") job.join() // wait for the coroutine to finish // async: returns a Deferred<T>, use .await() to get the result val deferred: Deferred<Int> = async { delay(500L) 21 } println("Answer: ${deferred.await() * 2}")}
Suspend Functions
Functions that can be paused and resumed without blocking a thread.
// suspend functions can call other suspend functions and be paused/resumedsuspend fun fetchUser(id: Int): String { delay(300) // non-blocking delay, suspends the coroutine return "User$id"}suspend fun fetchAndPrint(id: Int) { val user = fetchUser(id) // suspension point println(user)}// suspend functions can only be called from a coroutine or another suspend funfun main() = runBlocking { fetchAndPrint(1)}
Coroutine Dispatchers
Controlling which thread(s) a coroutine runs on.
- Dispatchers.Main- Runs on the UI thread (Android/desktop UI frameworks); for UI updates
- Dispatchers.IO- Optimized for blocking I/O like network calls and file access, large thread pool
- Dispatchers.Default- Optimized for CPU-intensive work (sorting, parsing), sized to CPU cores
- Dispatchers.Unconfined- Starts in the caller's thread, resumes in whatever thread the suspension used
- withContext(Dispatcher)- Switches the coroutine's dispatcher for a block, then switches back
- newSingleThreadContext()- Creates a dedicated single-thread dispatcher for confinement
Structured Concurrency
Scoping coroutines so they can't outlive their parent unexpectedly.
import kotlinx.coroutines.*class UserRepository { // CoroutineScope tied to this class's lifecycle private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) fun loadUsers() { scope.launch { val users = fetchUsers() // child coroutine println(users) } } fun cancelAll() { scope.cancel() // cancels all children started in this scope }}suspend fun fetchUsers(): List<String> = coroutineScope { // coroutineScope suspends until all children complete; propagates errors val a = async { fetchUser(1) } val b = async { fetchUser(2) } listOf(a.await(), b.await())}
Flow Basics
Cold asynchronous streams of values built on coroutines.
import kotlinx.coroutines.flow.*fun countdown(from: Int): Flow<Int> = flow { for (i in from downTo 1) { delay(100) emit(i) // suspending emission of the next value }}suspend fun main() { countdown(3) .map { it * 10 } .filter { it > 10 } .collect { value -> println(value) } // terminal operator, starts the flow}// StateFlow: hot, state-holder flow with an initial valueval state = MutableStateFlow(0)state.value = 1
Exception Handling & SupervisorJob
How exceptions propagate in coroutine hierarchies and how to isolate failures.
import kotlinx.coroutines.*// CoroutineExceptionHandler catches uncaught exceptions from launch (not async)val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception")}fun main() = runBlocking { // With a regular Job, one child failing cancels all siblings val scope = CoroutineScope(Job()) scope.launch(handler) { launch { throw RuntimeException("child 1 failed") } launch { delay(1000); println("never printed, sibling was cancelled") } } delay(200) // SupervisorJob: a failing child does NOT cancel its siblings val supervisor = CoroutineScope(SupervisorJob()) supervisor.launch(handler) { throw RuntimeException("isolated failure") } supervisor.launch { delay(300); println("still runs") } delay(500) // async exceptions are stored and only rethrown on await() val deferred = scope.async { throw IllegalStateException("boom") } try { deferred.await() } catch (e: IllegalStateException) { println("await rethrew: ${e.message}") }}
Cooperative Cancellation
Coroutines must actively check for cancellation; CPU-bound loops don't cancel automatically.
import kotlinx.coroutines.*fun main() = runBlocking { val job = launch(Dispatchers.Default) { var i = 0 while (isActive) { // checks cancellation cooperatively if (i % 1_000_000 == 0) ensureActive() // throws CancellationException if cancelled i++ } } delay(100) job.cancelAndJoin() // requests cancellation and suspends until it finishes // withTimeout throws TimeoutCancellationException if the block runs too long try { withTimeout(500) { delay(1000) } } catch (e: TimeoutCancellationException) { println("timed out") } // withTimeoutOrNull returns null instead of throwing val result = withTimeoutOrNull(500) { delay(1000); "done" } println(result) // null // finally blocks still run on cancellation, but suspend calls inside // finally need withContext(NonCancellable) to actually execute launch { try { delay(1000) } finally { withContext(NonCancellable) { delay(100) // cleanup that must complete even though we're cancelled println("cleanup done") } } }.cancelAndJoin()}
Channels
Hot, concurrency-safe pipes for passing values between coroutines.
import kotlinx.coroutines.*import kotlinx.coroutines.channels.*fun main() = runBlocking { val channel = Channel<Int>(capacity = 2) // buffered channel val producer = launch { for (x in 1..5) { channel.send(x) // suspends when the buffer is full } channel.close() // signals no more elements to consumers } val consumer = launch { for (value in channel) { // iterates until the channel is closed println("received $value") } } joinAll(producer, consumer) // produce { } builds a channel-backed coroutine, common producer pattern val squares = produce { for (x in 1..3) send(x * x) } squares.consumeEach { println(it) }}
Advanced Flow Operators
Combining flows, controlling concurrency, and buffering.
import kotlinx.coroutines.flow.*import kotlinx.coroutines.*suspend fun main() { val names = flowOf("Ana", "Bo") val ages = flowOf(30, 25) // combine: emits whenever either source flow emits a new value names.combine(ages) { name, age -> "$name is $age" } .collect { println(it) } // flatMapLatest: cancels the previous inner flow when a new value arrives flowOf(1, 2, 3) .flatMapLatest { value -> flow { delay(100) emit(value * 10) } } .collect { println(it) } // buffer: lets the producer run ahead of a slow collector, decoupling stages flow { for (i in 1..3) { delay(100); emit(i) } }.buffer().collect { delay(300); println(it) } // catch + retry for resilient upstream error handling flow<Int> { throw RuntimeException("network error") } .retry(2) { e -> e is RuntimeException } .catch { e -> println("gave up: ${e.message}") } .collect()}
CoroutineContext Elements
The building blocks that make up a coroutine's execution context.
- Job / SupervisorJob- Controls lifecycle and cancellation propagation; SupervisorJob isolates child failures
- CoroutineDispatcher- Determines which thread(s) the coroutine's code runs on
- CoroutineName- Human-readable name for debugging, shows up in thread dumps
- CoroutineExceptionHandler- Last-resort handler for uncaught exceptions in launch-rooted coroutines
- context + context- Combines elements with the + operator; later elements override earlier ones of the same key
- coroutineContext[Job]- Reads a specific element out of the current coroutine's context
- NonCancellable- A special context that suppresses cancellation, used for guaranteed cleanup
Prefer structured concurrency (coroutineScope, viewModelScope, or a scope tied to a lifecycle) over GlobalScope.launch — GlobalScope coroutines outlive their caller and are a common source of leaks and untracked crashes.