Skip to Content
Kotlin⚡ Kotlin CoroutinesException Handling

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 failed

SupervisorScope: 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 completed

5. 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 called

Cancellation 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: -1

retry 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: 42

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

ScopeException Behavior
coroutineScopeOne fails -> all cancel
supervisorScopeFailures isolated
launchPropagates to parent
asyncDeferred until await()
HandlerWorks with
try-catchInside coroutine, or around await()
CoroutineExceptionHandlerlaunch (not async)
Flow catchFlow emissions

📝 Tóm tắt

  • Dùng try-catch bên trong coroutines hoặc xung quanh await()
  • CoroutineExceptionHandler cho uncaught exceptions từ launch
  • supervisorScope khi tasks độc lập
  • CancellationException nên được re-throw
  • NonCancellable cho critical cleanup
  • Flow: dùng catchretry operators
Last updated on