在簡單的kotlin中的異常處理
try {
// some code
throw RuntimeException("RuntimeException in 'some code'")
} catch (exception: Exception) {
println("Handle $exception")
}
// Output:
// Handle java.lang.RuntimeException: RuntimeException in 'some code'
fun main() {
try {
functionThatThrows()
} catch (exception: Exception) {
println("Handle $exception")
}
}
fun functionThatThrows() {
// some code
throw RuntimeException("RuntimeException in regular function")
}
// Output
// Handle java.lang.RuntimeException: RuntimeException in regular function
那么在協(xié)程中又是如何處理異常呢?
在Coroutines中使用try-catch
fun main() {
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
try {
throw RuntimeException("RuntimeException in coroutine")
} catch (exception: Exception) {
println("Handle $exception")
}
}
Thread.sleep(100)
}
// Output
// Handle java.lang.RuntimeException: RuntimeException in coroutine
但是如果我們修改代碼為如下情況
fun main() {
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
try {
launch {
throw RuntimeException("RuntimeException in nested coroutine")
}
} catch (exception: Exception) {
println("Handle $exception")
}
}
Thread.sleep(100)
}
// Output
// Exception in thread "main" java.lang.RuntimeException: RuntimeException in nested coroutine
異常并沒有被catch住。這是因為協(xié)程自身并不能過通過try catch來捕獲異常。
協(xié)程是一種具有父子關(guān)系的Job層級結(jié)構(gòu)。
如果有安裝過CoroutineExceptionHandler的話,傳遞的異常通常會被CoroutineExceptionHandler處理,如果沒有安裝過,異常將會由線程的未捕獲異常處理器處理。
總結(jié)1:如果協(xié)程內(nèi)部沒有通過try-catch處理異常,那么異常并不會被重新拋出或者被外部的try-catch捕獲。異常將會在job層級結(jié)構(gòu)中向上傳遞,將會被安裝的CoroutineExceptionHandler
處理,如果沒有安裝過,異常將會被線程的未捕獲的異常處理器處理。
Coroutine Exception Handler
fun main() {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
launch(coroutineExceptionHandler) {
throw RuntimeException("RuntimeException in nested coroutine")
}
}
Thread.sleep(100)
}
// Output
// Exception in thread "DefaultDispatcher-worker-2" java.lang.RuntimeException: RuntimeException in nested coroutine
我們?nèi)绱诵薷拇a,異常仍然沒有被捕獲。那是因為在子協(xié)程內(nèi)安裝CoroutineExceptionHandler是不會有任何效果的。
只有在作用域內(nèi)或者頂級協(xié)程內(nèi)安裝CoroutineExceptionHandler才會有效果。
// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...
或者
// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...
效果如下
// ..
// Output:
// Handle java.lang.RuntimeException: RuntimeException in nested coroutine in CoroutineExceptionHandler
總結(jié)2:只有安裝在CoroutineScope
或者頂級協(xié)程的CoroutineExceptionHandler
才會生效
try-cach VS CoroutineExceptionHandler
針對如何選擇try-cach 和CoroutineExceptionHandler來處理異常,官方文檔也給出了如下建議:
CoroutineExceptionHandler是全局異常捕獲的最后手段,你不能在CoroutineExceptionHandler中恢復你的操作,當相應的異常處理器被調(diào)用的時候,協(xié)程已經(jīng)完成了。通常,handler是用于打印異常,顯示一些錯誤信息,終止,還有重啟應用。
-
如果你需要在特定的代碼中處理異常,那么try-cach是推薦的異常處理方法,這可以幫助你避免因為異常而終止協(xié)程,以此繼續(xù)進行重試操作或者其他操作。
總結(jié)3:如果你想要重試某些操作,或者在協(xié)程完成之前執(zhí)行某些操作,那么可以考慮使用try-cach。需要注意的是,在協(xié)程內(nèi)部使用了try-cach捕獲該異常之后,那么這個異常將不會再向上傳遞,也不能使用利用結(jié)構(gòu)性并發(fā)的取消函數(shù)。在i協(xié)程因異常結(jié)束需要打印異常信息的時候可以考慮使用
CoroutineExceptionHandler
。
launch{} VS async{}
fun main() {
val topLevelScope = CoroutineScope(SupervisorJob())
topLevelScope.async {
throw RuntimeException("RuntimeException in async coroutine")
}
Thread.sleep(100)
}
// No output
在上述例子中,并沒用打印異常。對于async來說,產(chǎn)生的異常同樣會馬上向上傳遞,只不過和launch相反的是,異常并不會被CoroutineExceptionHandler和線程的未捕獲異常處理器處理。async內(nèi)拋出的異常會被封裝在Deferred內(nèi)部,當我們在通過.await()獲取結(jié)果的時候,異常會被重新拋出。
注意:只有當async協(xié)程是頂級協(xié)程的時候,async內(nèi)的異常才會被封裝在Deferred內(nèi)部,除此以外,異常會被馬上向上傳遞,并且交給CoroutineExceptionHandler處理或者線程的未捕獲異常處理器處理,即使你沒有調(diào)用.await()方法。
fun main() {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val topLevelScope = CoroutineScope(SupervisorJob() + coroutineExceptionHandler)
topLevelScope.launch {
async {
throw RuntimeException("RuntimeException in async coroutine")
}
}
Thread.sleep(100)
}
// Output
// Handle java.lang.RuntimeException: RuntimeException in async coroutine in CoroutineExceptionHandler
總結(jié)4:對于launch和async未捕獲的異常都會被馬上向上傳遞,然而,如果頂級協(xié)程是由launch啟動,那么異常將會由CoroutineExceptionHandler
或者線程的未捕獲異常處理器處理,如果頂級協(xié)程由async啟動,那么異常將會被封裝進Deferred,在調(diào)用.await
時候重新拋出。
coroutineScope{}的異常處理特性
在文章開頭,在try-cach內(nèi)啟動的協(xié)程內(nèi)的異常不能被捕獲,但如果在失敗的協(xié)程外部套上coroutineScope{}函數(shù),那就會不太一樣了:
fun main() {
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
try {
coroutineScope {
launch {
throw RuntimeException("RuntimeException in nested coroutine")
}
}
} catch (exception: Exception) {
println("Handle $exception in try/catch")
}
}
Thread.sleep(100)
}
// Output
// Handle java.lang.RuntimeException: RuntimeException in nested coroutine in try/catch
catch成功捕獲了異常,這是因為coroutineScope{}將失敗的子協(xié)程內(nèi)部的異常拋出,而沒有繼續(xù)向上傳遞。
總結(jié)5: coroutineScope{}會重新拋出失敗子協(xié)程內(nèi)的異常而不是將其繼續(xù)向上傳遞,這樣我就可以自己處理失敗子協(xié)程的異常了。
supervisorScope{}異常處理特性
fun main() {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
val job1 = launch {
println("starting Coroutine 1")
}
supervisorScope {
val job2 = launch(coroutineExceptionHandler) {
println("starting Coroutine 2")
throw RuntimeException("Exception in Coroutine 2")
}
val job3 = launch {
println("starting Coroutine 3")
}
}
}
Thread.sleep(100)
}
// Output
// starting Coroutine 1
// starting Coroutine 2
// Handle java.lang.RuntimeException: Exception in Coroutine 2 in CoroutineExceptionHandler
// starting Coroutine 3
對于supervisorScope既不會重新拋出失敗的子協(xié)程的異常也不會將異常繼續(xù)向上傳遞。
對于job層級結(jié)構(gòu),異常會一直向上傳遞直到遇到頂級協(xié)程作用域或者SupervisorJob為止。
在supervisorScope內(nèi)直接啟動的作為頂級協(xié)程將異常封裝進Deferred對象
// ... other code is identical to example above
supervisorScope {
val job2 = async {
println("starting Coroutine 2")
throw RuntimeException("Exception in Coroutine 2")
}
}
// ...
// Output:
// starting Coroutine 1
// starting Coroutine 2
// starting Coroutine 3
只有在調(diào)用.await()的時候才會重新拋出異常。
總結(jié)6:作用域函數(shù)supervisorScope{}
會在job層級中安裝一個獨立的新的子作用域,并使用SupervisorJob
作為該作用域的job,這個新的作用域并不會將異常繼續(xù)向上傳遞,異常將由它自己處理。在supervisorScope
內(nèi)直接啟動的協(xié)程將作為頂級協(xié)程。頂級協(xié)程在由launch或者async啟動的時候,它的表現(xiàn)和作為子協(xié)程時的表現(xiàn)將有所不同。