Bỏ qua để đến nội dung

Exception Handling

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.

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!")
}
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")
}

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!
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}")
}
}
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
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
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}")
}
}
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!

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
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()
}
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}")
}
}
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
}
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
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
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()
}
}
}
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

  • 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