協程例外處理#

協程中的例外如果沒有妥善處理,可能導致整個作用域的協程被取消,或是例外靜默地消失。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 不適用於 asyncasync 的例外會被封裝在 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#

coroutineScopesupervisorScope
子協程失敗時取消所有兄弟協程,例外向上拋只取消自己,例外不向上傳播
適用場景任一步驟失敗就整體放棄各子任務獨立,互不影響
典型例子多步驟交易(全部成功才算完成)同時載入多個獨立資源
子協程例外捕捉方式coroutineScope 外用 try-catch在各子協程安裝 CoroutineExceptionHandler

Reference#

https://kotlinlang.org/docs/exception-handling.html