協程例外處理#
協程中的例外如果沒有妥善處理,可能導致整個作用域的協程被取消,或是例外靜默地消失。Kotlin 提供三種策略,各自適用不同情境:
| 策略 | 適用情境 |
|---|
try-catch | 協程 內部 可預期的錯誤,就地處理 |
CoroutineExceptionHandler | 協程 外部 統一攔截未捕捉的例外(日誌、通知) |
coroutineScope / supervisorScope | 控制多個子協程之間的 錯誤傳播範圍 |
1. try-catch 在協程中#
try-catch 在協程裡的用法和普通函數完全相同,可以直接包住可能出錯的程式碼。suspend 函數拋出的例外也可以用 try-catch 捕捉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
delay(100)
throw RuntimeException("協程內部錯誤")
} catch (e: RuntimeException) {
println("捕捉到錯誤:${e.message}")
// 在這裡處理錯誤,協程繼續執行或結束
}
}
job.join()
// 輸出:捕捉到錯誤:協程內部錯誤
}
|
2. CoroutineExceptionHandler:統一攔截未捕捉的例外#
當例外沒有被 try-catch 處理,就會從協程往上傳播。CoroutineExceptionHandler 是最後一道防線,統一接收這些未捕捉的例外,適合用來記錄錯誤日誌。
建立方式:建立 handler 後傳給 launch 當作 context 參數。handler 必須安裝在 根協程(沒有父協程、或父協程是 SupervisorJob)才會被呼叫。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("全域捕捉:${exception.message}")
}
// supervisorScope 讓 launch 成為獨立的根協程,handler 才能生效
supervisorScope {
val job = launch(handler) {
delay(100)
throw IllegalStateException("發生嚴重錯誤") // 沒有 try-catch
}
job.join()
}
// 輸出:全域捕捉:發生嚴重錯誤
}
|
3. async 的例外:在 await() 處捕捉#
CoroutineExceptionHandler 不適用於 async。async 的例外會被封裝在 Deferred 物件裡,一直到呼叫 .await() 時才拋出,必須在 await() 外面用 try-catch 捕捉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async {
delay(100)
throw RuntimeException("async 內部錯誤")
"不會回傳"
}
try {
val result = deferred.await() // 例外在這裡才拋出
println(result)
} catch (e: RuntimeException) {
println("await 捕捉:${e.message}")
}
// 輸出:await 捕捉:async 內部錯誤
}
|
4. coroutineScope vs. supervisorScope:錯誤傳播範圍#
預設行為:一個失敗,全部取消#
在普通的 coroutineScope 下,一個子協程拋出例外,會向上傳播給父協程,父協程再把 所有其他子協程一併取消,最後把例外拋給呼叫方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import kotlinx.coroutines.*
fun main() = runBlocking {
try {
coroutineScope {
val job1 = launch {
delay(100)
throw RuntimeException("子協程 1 失敗")
}
val job2 = launch {
delay(300)
println("子協程 2 成功完成") // 不會印出:100ms 時被取消
}
joinAll(job1, job2)
}
} catch (e: RuntimeException) {
println("例外被捕捉:${e.message}")
}
// 輸出:例外被捕捉:子協程 1 失敗
}
|
supervisorScope:改變取消策略#
supervisorScope 改變了錯誤傳播規則:子協程失敗時,只有自己被取消,其他兄弟協程不受影響,supervisorScope 本身也不會拋出例外。
子協程的例外不再向上傳播,必須用 CoroutineExceptionHandler 來捕捉:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, e ->
println("子協程 1 例外:${e.message}")
}
supervisorScope {
val job1 = launch(handler) { // 安裝 handler 捕捉此協程的例外
delay(100)
throw RuntimeException("子協程 1 失敗")
}
val job2 = launch {
delay(300)
println("子協程 2 成功完成") // 正常執行,不受影響
}
joinAll(job1, job2)
}
// 輸出:
// 子協程 1 例外:子協程 1 失敗
// 子協程 2 成功完成
}
|
supervisorScope 會等待所有子協程完成後才返回,所以兩行輸出都能出現。
coroutineScope vs. supervisorScope#
| coroutineScope | supervisorScope |
|---|
| 子協程失敗時 | 取消所有兄弟協程,例外向上拋 | 只取消自己,例外不向上傳播 |
| 適用場景 | 任一步驟失敗就整體放棄 | 各子任務獨立,互不影響 |
| 典型例子 | 多步驟交易(全部成功才算完成) | 同時載入多個獨立資源 |
| 子協程例外捕捉方式 | 在 coroutineScope 外用 try-catch | 在各子協程安裝 CoroutineExceptionHandler |
Reference#
https://kotlinlang.org/docs/exception-handling.html