Coroutine Exception Handling and Supervision
Handle errors in coroutines: CoroutineExceptionHandler, supervisorScope, try-catch, and error propagation.
import kotlinx.coroutines.*
fun main() = runBlocking {
// 1. try-catch in coroutine
println("--- try-catch ---")
val result = try {
withContext(Dispatchers.Default) {
riskyOperation("test")
}
} catch (e: Exception) {
println("Caught: ${e.message}")
"fallback"
}
println("Result: $result")
// 2. CoroutineExceptionHandler (only for launch, not async)
println("\n--- ExceptionHandler ---")
val handler = CoroutineExceptionHandler { _, ex ->
println("Handler caught: ${ex.message}")
}
val job = launch(handler) {
throw RuntimeException("Unhandled!")
}
job.join()
// 3. supervisorScope — children fail independently
println("\n--- Supervisor ---")
supervisorScope {
val child1 = launch {
delay(100)
throw RuntimeException("Child 1 failed")
}
val child2 = launch {
delay(200)
println("Child 2 completed successfully")
}
val child3 = async {
delay(150)
"Child 3 result"
}
try { child1.join() } catch (e: Exception) { /* expected */ }
child2.join()
println("Child 3: ${child3.await()}")
}
// 4. async exception — deferred until await()
println("\n--- Async exception ---")
coroutineScope {
val deferred = async {
delay(100)
throw ArithmeticException("Division by zero")
}
try {
deferred.await()
} catch (e: ArithmeticException) {
println("Async caught: ${e.message}")
}
}
// 5. Resilient parallel calls
println("\n--- Resilient parallel ---")
val results = supervisorScope {
val calls = listOf("a", "b", "fail", "c").map { id ->
async {
try {
fetchData(id)
} catch (e: Exception) {
"Error($id): ${e.message}"
}
}
}
calls.map { it.await() }
}
println("Results: $results")
// 6. Cancellation vs Exception
println("\n--- Cancellation ---")
val cancelJob = launch {
try {
repeat(100) { i ->
println("Working $i")
delay(50)
}
} catch (e: CancellationException) {
println("Cancelled (this is normal)")
throw e // Always re-throw CancellationException!
} finally {
// Cleanup runs even on cancellation
withContext(NonCancellable) {
println("Cleanup...")
delay(100)
println("Cleanup done")
}
}
}
delay(120)
cancelJob.cancelAndJoin()
println("Job cancelled")
}
suspend fun riskyOperation(input: String): String {
delay(50)
if (input == "test") return "success"
throw IllegalArgumentException("Bad input")
}
suspend fun fetchData(id: String): String {
delay(100)
if (id == "fail") throw RuntimeException("Service unavailable")
return "data-$id"
}Use Cases
- Robust error handling in concurrent operations
- Partial failure tolerance in parallel calls
- Graceful shutdown with cleanup
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
SupervisorJob and Error Isolation
Isolate coroutine failures with SupervisorJob: independent child coroutines, partial failure handling.
Best for: Independent failure isolation in microservices
Coroutines — launch, async, and Structured Concurrency
Write concurrent code with Kotlin coroutines: launch, async/await, structured concurrency, and dispatchers.
Best for: Parallel API calls in backend services
Flow — Reactive Streams with Coroutines
Build reactive pipelines with Kotlin Flow: emit, collect, transform, combine, and error handling.
Best for: Streaming data processing pipelines
Channels — Producer-Consumer with Coroutines
Communicate between coroutines with Channels: produce, actor pattern, fan-out/fan-in, and select.
Best for: Producer-consumer patterns in coroutines