Exception Handling trong Coroutines
1. Giới thiệu
Exception handling trong coroutines khác với code thông thường do structured concurrency. Exceptions propagate theo hierarchy và có thể cancel siblings.
2. Basic Exception Handling
try-catch trong coroutine
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
try {
riskyOperation()
} catch (e: Exception) {
println("Caught in coroutine: ${e.message}")
}
}
}
suspend fun riskyOperation() {
delay(100)
throw RuntimeException("Something went wrong!")
}try-catch KHÔNG work xung quanh launch
import kotlinx.coroutines.*
fun main() = runBlocking {
// ❌ KHÔNG catch được exception từ launch
try {
launch {
throw RuntimeException("Failed!")
}
} catch (e: Exception) {
println("This won't catch it!")
}
delay(100)
println("This might not print due to uncaught exception")
}3. CoroutineExceptionHandler
Handler cuối cùng cho uncaught exceptions:
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { context, exception ->
println("Caught in handler: ${exception.message}")
println("Coroutine: ${context[CoroutineName]}")
}
val scope = CoroutineScope(SupervisorJob() + handler + CoroutineName("MainScope"))
scope.launch(CoroutineName("FailingJob")) {
throw RuntimeException("Oops!")
}
scope.launch(CoroutineName("SuccessJob")) {
delay(100)
println("This still runs with SupervisorJob!")
}
delay(200)
}
// Output:
// Caught in handler: Oops!
// Coroutine: CoroutineName(FailingJob)
// This still runs with SupervisorJob!Handler rules
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("Handler: ${e.message}")
}
// ✅ Handler works with launch
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
throw RuntimeException("launch exception")
}
delay(100)
// ❌ Handler KHÔNG work với async - dùng try-catch với await()
val deferred = scope.async {
throw RuntimeException("async exception")
}
try {
deferred.await()
} catch (e: Exception) {
println("Caught from async: ${e.message}")
}
}4. Exception Propagation
Default behavior: fail fast
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
coroutineScope {
launch {
delay(50)
throw RuntimeException("Child 1 failed")
}
launch {
try {
delay(Long.MAX_VALUE)
} catch (e: CancellationException) {
println("Child 2 cancelled due to sibling failure")
throw e // Re-throw to complete cancellation
}
}
}
} catch (e: RuntimeException) {
println("Parent caught: ${e.message}")
}
}
// Output:
// Child 2 cancelled due to sibling failure
// Parent caught: Child 1 failedSupervisorScope: isolate failures
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("Handler caught: ${e.message}")
}
supervisorScope {
launch(handler) {
delay(50)
throw RuntimeException("Child 1 failed")
}
launch {
delay(100)
println("Child 2 completed successfully!")
}
}
println("Supervisor scope completed")
}
// Output:
// Handler caught: Child 1 failed
// Child 2 completed successfully!
// Supervisor scope completed5. async Exception Handling
import kotlinx.coroutines.*
fun main() = runBlocking {
// async defers exception until await()
val deferred = async {
delay(100)
throw RuntimeException("Async failed")
}
// Exception thrown here
try {
val result = deferred.await()
} catch (e: RuntimeException) {
println("Caught at await: ${e.message}")
}
}Parallel async với error handling
import kotlinx.coroutines.*
suspend fun fetchAllData(): Result<Data> = coroutineScope {
try {
val users = async { fetchUsers() }
val posts = async { fetchPosts() }
Result.success(Data(users.await(), posts.await()))
} catch (e: Exception) {
Result.failure(e)
}
}
data class Data(val users: List<String>, val posts: List<String>)
suspend fun fetchUsers(): List<String> {
delay(100)
return listOf("Alice", "Bob")
}
suspend fun fetchPosts(): List<String> {
delay(150)
throw RuntimeException("Network error!")
}
fun main() = runBlocking {
when (val result = fetchAllData()) {
is Result -> result.fold(
onSuccess = { println("Data: $it") },
onFailure = { println("Error: ${it.message}") }
)
}
}
// Output: Error: Network error!6. CancellationException
Cancellation là đặc biệt - không được coi là failure:
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("Handler: ${e.message}") // NOT called for cancellation
}
val job = launch(handler) {
try {
delay(Long.MAX_VALUE)
} catch (e: CancellationException) {
println("Caught cancellation: ${e.message}")
throw e // Re-throw is OK (và recommended)
}
}
delay(100)
job.cancel(CancellationException("Cancelled by user"))
job.join()
println("Job cancelled, but no exception handler called")
}
// Output:
// Caught cancellation: Cancelled by user
// Job cancelled, but no exception handler calledCancellation trong finally
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
delay(Long.MAX_VALUE)
} finally {
// Cleanup code
println("Cleaning up...")
// ❌ Suspending calls throw here normally
// delay(100) // throws CancellationException
// ✅ Use withContext(NonCancellable) for cleanup
withContext(NonCancellable) {
delay(100)
println("Cleanup complete")
}
}
}
delay(100)
job.cancelAndJoin()
}7. Retry Patterns
Simple retry
import kotlinx.coroutines.*
suspend fun <T> retry(
times: Int = 3,
delayMs: Long = 1000,
block: suspend () -> T
): T {
var lastException: Exception? = null
repeat(times) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
println("Attempt ${attempt + 1} failed: ${e.message}")
if (attempt < times - 1) {
delay(delayMs)
}
}
}
throw lastException!!
}
suspend fun unreliableCall(): String {
if (Math.random() < 0.7) {
throw RuntimeException("Network error")
}
return "Success!"
}
fun main() = runBlocking {
try {
val result = retry(times = 5, delayMs = 500) {
unreliableCall()
}
println("Result: $result")
} catch (e: Exception) {
println("All attempts failed: ${e.message}")
}
}Exponential backoff
import kotlinx.coroutines.*
import kotlin.math.pow
suspend fun <T> retryWithBackoff(
times: Int = 3,
initialDelayMs: Long = 100,
maxDelayMs: Long = 10000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelayMs
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
println("Attempt ${attempt + 1} failed, retrying in ${currentDelay}ms")
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelayMs)
}
}
return block() // Last attempt
}8. Flow Error Handling
catch operator
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun riskyFlow(): Flow<Int> = flow {
emit(1)
emit(2)
throw RuntimeException("Flow error!")
emit(3) // Never reached
}
fun main() = runBlocking {
riskyFlow()
.catch { e ->
println("Caught: ${e.message}")
emit(-1) // Emit fallback value
}
.collect { println("Value: $it") }
}
// Output:
// Value: 1
// Value: 2
// Caught: Flow error!
// Value: -1retry operator
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
var attempts = 0
fun unreliableFlow(): Flow<Int> = flow {
attempts++
if (attempts < 3) {
throw RuntimeException("Attempt $attempts failed")
}
emit(42)
}
fun main() = runBlocking {
unreliableFlow()
.retry(3) { e ->
println("Retrying due to: ${e.message}")
delay(100)
true // true = retry, false = don't retry
}
.catch { println("All retries failed: ${it.message}") }
.collect { println("Result: $it") }
}
// Output:
// Retrying due to: Attempt 1 failed
// Retrying due to: Attempt 2 failed
// Result: 429. Best Practices
import kotlinx.coroutines.*
// ✅ Handle exceptions at the right level
class Repository {
suspend fun fetchData(): Result<Data> {
return try {
Result.success(actualFetch())
} catch (e: NetworkException) {
Result.failure(e)
}
}
}
// ✅ Use supervisorScope for independent tasks
suspend fun loadDashboard() = supervisorScope {
val news = async { fetchNews() } // Independent
val weather = async { fetchWeather() } // Independent
DashboardData(
news = runCatching { news.await() }.getOrDefault(emptyList()),
weather = runCatching { weather.await() }.getOrNull()
)
}
// ✅ Always handle CancellationException properly
suspend fun doWork() {
try {
// work
} catch (e: CancellationException) {
// Cleanup if needed
throw e // Always re-throw!
} catch (e: Exception) {
// Handle other exceptions
}
}
// ✅ Use NonCancellable for critical cleanup
suspend fun saveAndClose(resource: Resource) {
try {
resource.use()
} finally {
withContext(NonCancellable) {
resource.save()
resource.close()
}
}
}10. Summary Table
| Scope | Exception Behavior |
|---|---|
coroutineScope | One fails -> all cancel |
supervisorScope | Failures isolated |
launch | Propagates to parent |
async | Deferred until await() |
| Handler | Works with |
|---|---|
try-catch | Inside coroutine, or around await() |
CoroutineExceptionHandler | launch (not async) |
Flow catch | Flow emissions |
📝 Tóm tắt
- Dùng
try-catchbên trong coroutines hoặc xung quanhawait() CoroutineExceptionHandlercho uncaught exceptions từlaunchsupervisorScopekhi tasks độc lậpCancellationExceptionnên được re-throwNonCancellablecho critical cleanup- Flow: dùng
catchvàretryoperators
Last updated on