kotlinadvanced

Coroutine Exception Handling and Supervision

Handle errors in coroutines: CoroutineExceptionHandler, supervisorScope, try-catch, and error propagation.

kotlin
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.