Coroutine Context, Cancellation and Exceptions
|
This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Building on Coroutines Basics, this page covers what actually
determines where a coroutine runs, how it is cancelled cleanly, and how it handles failure — three concerns
Kotlin bundles together as a coroutine’s CoroutineContext.
CoroutineContext
Every coroutine carries a CoroutineContext — a set-like collection of elements (a Job, a Dispatcher, a
name, an exception handler) that together describe how and where it runs. Context elements combine with +:
val context = Dispatchers.IO + CoroutineName("data-loader")
scope.launch(context) { /* ... */ }
Dispatchers
A Dispatcher decides which thread(s) a coroutine’s code actually runs on — suspending does not, by itself,
guarantee any particular thread:
| Dispatcher | Use |
|---|---|
|
CPU-bound work (sorting, parsing, computation) — backed by a thread pool sized to the number of CPU cores. |
|
blocking I/O (network calls, file access, JDBC) — a larger, elastic thread pool designed for many simultaneously-blocked threads. |
|
the UI thread on a platform that has one (Android’s main thread) — only available when the corresponding platform artifact is on the classpath. |
suspend fun loadAndRender() {
val data = withContext(Dispatchers.IO) { // switches to the IO dispatcher for this block only...
fetchFromNetwork()
}
withContext(Dispatchers.Main) { // ...then back to Main to update the UI
render(data)
}
}
withContext suspends the calling coroutine, runs the block on the requested dispatcher, and resumes back on
the original one automatically once the block completes — there is no manual "post back to the UI thread"
callback to write.
Cancellation and Timeouts
Cancellation in coroutines is cooperative: a coroutine must actually check for cancellation (which every
suspending function in kotlinx.coroutines, like delay, does automatically) to actually stop. withTimeout
cancels its block automatically if it runs too long, throwing TimeoutCancellationException; withTimeoutOrNull
does the same but returns null instead of throwing:
suspend fun fetchWithDeadline(): User? =
withTimeoutOrNull(2_000) { // milliseconds
fetchUser(1) // cancelled, and this call returns null, if it takes >2s
}
A long-running, non-suspending computation (a tight loop with no delay/suspension point inside it) will
not respond to cancellation on its own — ensureActive() or isActive must be checked explicitly inside
such a loop for it to cooperate.
SupervisorJob
By default (structured concurrency, see
Coroutines Basics), one child’s failure cancels every sibling
and the parent. A SupervisorJob changes that: a child’s failure is isolated to that child, leaving its
siblings running — the shape needed for, say, a screen with several independent widgets that should not all
crash because one of them failed to load:
val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
supervisorScope.launch { failingWidget() } // this one fails...
supervisorScope.launch { healthyWidget() } // ...but this one is unaffected
CoroutineExceptionHandler
An uncaught exception from a launch coroutine (not async — a Deferred’s exception surfaces at `.await()
instead) is, by default, propagated up and can crash the application the same way an uncaught exception on any
thread would. A CoroutineExceptionHandler, installed on the top-level scope, intercepts it instead:
val handler = CoroutineExceptionHandler { _, exception ->
log.error("unhandled coroutine failure", exception)
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
scope.launch { throw IllegalStateException("boom") } // caught by "handler" instead of crashing
CoroutineExceptionHandler only has an effect on a root coroutine (one launched directly in a scope, not
nested inside another coroutine) and is commonly paired with SupervisorJob for exactly the "many independent
tasks, log and continue" pattern above.
See Also
-
Coroutines Basics —
launch/async/CoroutineScopeand default structured-concurrency propagation. -
Flows — dispatchers and cancellation in the context of a stream of values rather than a single result.
-
Kotlin for Android —
Dispatchers.Mainin an actual Android app.
References
-
Kotlin docs — Coroutine exceptions handling (
SupervisorJob,CoroutineExceptionHandler).