在kotlin协程中如何正确处理异常
在简单的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
那么在协程中又是如何处理异常呢?
在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住。这是因为协程自身并不能过通过try catch来捕获异常。
协程是一种具有父子关系的Job层级结构。
job 层级结构 异常传递如果有安装过CoroutineExceptionHandler的话,传递的异常通常会被CoroutineExceptionHandler处理,如果没有安装过,异常将会由线程的未捕获异常处理器处理。
总结1:如果协程内部没有通过try-catch处理异常,那么异常并不会被重新抛出或者被外部的try-catch捕获。异常将会在job层级结构中向上传递,将会被安装的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
我们如此修改代码,异常仍然没有被捕获。那是因为在子协程内安装CoroutineExceptionHandler是不会有任何效果的。
只有在作用域内或者顶级协程内安装CoroutineExceptionHandler才会有效果。
// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...
或者
// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...
效果如下
// ..
// Output:
// Handle java.lang.RuntimeException: RuntimeException in nested coroutine in CoroutineExceptionHandler
总结2:只有安装在CoroutineScope
或者顶级协程的CoroutineExceptionHandler
才会生效
try-cach VS CoroutineExceptionHandler
针对如何选择try-cach 和CoroutineExceptionHandler来处理异常,官方文档也给出了如下建议:
-
CoroutineExceptionHandler是全局异常捕获的最后手段,你不能在CoroutineExceptionHandler中恢复你的操作,当相应的异常处理器被调用的时候,协程已经完成了。通常,handler是用于打印异常,显示一些错误信息,终止,还有重启应用。
-
如果你需要在特定的代码中处理异常,那么try-cach是推荐的异常处理方法,这可以帮助你避免因为异常而终止协程,以此继续进行重试操作或者其他操作。
总结3:如果你想要重试某些操作,或者在协程完成之前执行某些操作,那么可以考虑使用try-cach。需要注意的是,在协程内部使用了try-cach捕获该异常之后,那么这个异常将不会再向上传递,也不能使用利用结构性并发的取消函数。在i协程因异常结束需要打印异常信息的时候可以考虑使用
CoroutineExceptionHandler
。
launch{} VS async{}
fun main() {
val topLevelScope = CoroutineScope(SupervisorJob())
topLevelScope.async {
throw RuntimeException("RuntimeException in async coroutine")
}
Thread.sleep(100)
}
// No output
在上述例子中,并没用打印异常。对于async来说,产生的异常同样会马上向上传递,只不过和launch相反的是,异常并不会被CoroutineExceptionHandler和线程的未捕获异常处理器处理。async内抛出的异常会被封装在Deferred内部,当我们在通过.await()获取结果的时候,异常会被重新抛出。
注意:只有当async协程是顶级协程的时候,async内的异常才会被封装在Deferred内部,除此以外,异常会被马上向上传递,并且交给CoroutineExceptionHandler处理或者线程的未捕获异常处理器处理,即使你没有调用.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
总结4:对于launch和async未捕获的异常都会被马上向上传递,然而,如果顶级协程是由launch启动,那么异常将会由CoroutineExceptionHandler
或者线程的未捕获异常处理器处理,如果顶级协程由async启动,那么异常将会被封装进Deferred,在调用.await
时候重新抛出。
coroutineScope{}的异常处理特性
在文章开头,在try-cach内启动的协程内的异常不能被捕获,但如果在失败的协程外部套上coroutineScope{}函数,那就会不太一样了:
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{}将失败的子协程内部的异常抛出,而没有继续向上传递。
总结5: coroutineScope{}会重新抛出失败子协程内的异常而不是将其继续向上传递,这样我就可以自己处理失败子协程的异常了。
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既不会重新抛出失败的子协程的异常也不会将异常继续向上传递。
对于job层级结构,异常会一直向上传递直到遇到顶级协程作用域或者SupervisorJob为止。
在supervisorScope内直接启动的作为顶级协程将异常封装进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
只有在调用.await()的时候才会重新抛出异常。
总结6:作用域函数supervisorScope{}
会在job层级中安装一个独立的新的子作用域,并使用SupervisorJob
作为该作用域的job,这个新的作用域并不会将异常继续向上传递,异常将由它自己处理。在supervisorScope
内直接启动的协程将作为顶级协程。顶级协程在由launch或者async启动的时候,它的表现和作为子协程时的表现将有所不同。