Skip to content
AgileLogics
7 min read

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) on Dispatchers.Main or Default — wrap it in withContext(Dispatchers.IO).
  • Swallowing CancellationException in a broad catch.