Dispatchers, Cancellation, and Timeouts
Prerequisites: Structured concurrency: async and scopes
Dispatchers — which threads run a coroutine
A dispatcher decides which thread or thread pool a coroutine runs on:
| Dispatcher | Use for |
|---|---|
Dispatchers.Main |
UI updates (Android and desktop apps) |
Dispatchers.IO |
Blocking I/O — files, JDBC, blocking network calls |
Dispatchers.Default |
CPU-heavy work — parsing, sorting, image processing |
withContext switches dispatcher for a block of code and returns its
result:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
suspend fun readConfig(path: String): String = withContext(Dispatchers.IO) {
File(path).takeIf { it.exists() }?.readText() ?: "default config"
}
fun main() = runBlocking {
println(readConfig("does-not-exist.txt"))
val onWorker = withContext(Dispatchers.Default) {
Thread.currentThread().name.startsWith("DefaultDispatcher")
}
println(onWorker)
}
default config
true
The pattern to follow: a suspend function that does blocking work switches to the right dispatcher itself, so callers — including the UI — can call it safely from anywhere. This is called being main-safe.
Cancellation
Cancelling a coroutine asks it to stop. Cancellation is
cooperative: suspending functions like delay check for it
automatically.
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("Working $i")
delay(300)
}
}
delay(700)
job.cancelAndJoin()
println("Cancelled")
}
Working 0
Working 1
Working 2
Cancelled
launch returns a Job — a handle for cancelling or waiting on the
coroutine. Long-running loops that don't suspend should check
isActive or call ensureActive() so cancellation can take effect.
Timeouts
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
fun main() = runBlocking {
val fast = withTimeoutOrNull(500) {
delay(100)
"fast result"
}
val slow = withTimeoutOrNull(500) {
delay(2000)
"slow result"
}
println(fast)
println(slow)
}
fast result
null
withTimeoutOrNull returns null if the time runs out;
withTimeout throws a TimeoutCancellationException instead.
Cancellation is an exception
Cancellation works by throwing CancellationException inside the
coroutine. That has an important consequence:
// Don't: swallows cancellation, so the coroutine keeps running
try {
doWork()
} catch (e: Exception) {
log(e)
}
Catch specific exceptions, or rethrow CancellationException. The same
applies to runCatching, which catches everything.
Common mistakes
- Calling blocking code (JDBC,
File.readText,Thread.sleep) onDispatchers.MainorDefault— wrap it inwithContext(Dispatchers.IO). - Swallowing
CancellationExceptionin a broadcatch.