The abnormal stepping pit of kotlin coroutine async await and the correct posture of exception handling

I believe everyone is very familiar with using Kotlin to do some asynchronous operations, especially combined with some components of Jetpack, which makes it very convenient for us to write asynchronous tasks in Android development.

However, when using coroutines, I personally feel that exception handling is relatively time-consuming to understand, because some small pits will still be encountered during use. Here is a record of what I encountered before. pit.

Step on a pit of exception handling when using async await

The official documentation of exception handling for kotlin coroutines
Let's first look at the official examples:

insert image description here

It can be seen that in the sample code, the exception capture of the async- enabled coroutine is when await is called . Looking at the printed results, it is indeed captured, and there is no problem.

According to this sample code, we may use it naturally, such as writing the following code in ViewModel:

    fun testAsync() {
    
    
        viewModelScope.launch {
    
    
            val deferred = async {
    
    
                LogUtils.e("准备抛出异常")
                delay(1000)
                throw Exception("async 抛出了一个异常")
            }
            try {
    
    
                deferred.await()
            } catch (e: Exception) {
    
    
                LogUtils.e("在 await 处捕获到 async的异常了")
            }
            LogUtils.e("后续代码继续执行")
        }
    }

According to the official sample code, async exceptions are only thrown when await is called. Then we can catch the exception thrown by async when we call await, and the program will not crash.
Let's see it in action:

insert image description here

Here I call the above code when I enter the exception handling again, and I can find that the app actually crashed. Let's take a look at the printed log
insert image description here

It can be seen that we have caught the exception while awaiting, why does the App still crash?

If you look carefully at the comments in the official example, you will find these two words root coroutine
insert image description here

Say the result directly:

When async is used as the root coroutine , the exception encapsulated in the deferred object will be thrown when await is called.
If async is used as a sub-coroutine , the exception will not be thrown when await is called, but will be thrown immediately .

This is why we clearly perform a try catch in the place of await, but the program still crashes.
Because the exception that was thrown immediately was not handled, it just crashed.

It's just that the official document does not clearly explain what is abnormal as a sub-coroutine, so I have stepped on the pit before.

Here you may have doubts, no, look at the log and it is obvious that the exception is thrown when awaiting, how can you say that it was thrown immediately, and how to prove it.
I can only say:
insert image description here
it's very simple, we add a delay before await, just look at the log print.
The code is as follows, just like above, a delay is directly added.

    fun testAsync() {
    
    
        viewModelScope.launch {
    
    
            val deferred = async {
    
    
                LogUtils.e("准备抛出异常")
                delay(1000)
                throw Exception("async 抛出了一个异常")
            }
            /*加个延时 主要是验证异常是不是在await的时候抛出*/
            delay(2000)
            try {
    
    
                deferred.await()
            } catch (e: Exception) {
    
    
                LogUtils.e("在 await 处捕获到 async的异常了")
            }
            LogUtils.e("后续代码继续执行")
        }
    }

Let's take a look at the running situation again. You can see that the log of await's try catch has no chance to print. Why, because the above code has gone wrong, the program has crashed, and there is no chance to wait for 2 seconds to continue execution. The above code will be printed because it is executed very quickly, so it will give you an illusion that it is still an exception thrown during await.
insert image description here

Next, let's verify whether it is really an exception thrown when calling await when it is the top-level scope: the
same processing method, except that async becomes the top-level scope at this time

    fun testTopAsync() {
    
    
        /*顶级作用域的async*/
        val deferred = viewModelScope.async {
    
    
            LogUtils.e("准备抛出异常")
            delay(1000)
            throw Exception("async 抛出了一个异常")
        }

        viewModelScope.launch {
    
    
            /*加个延时 主要是验证异常是不是在await的时候抛出*/
            delay(2000)        
            try {
    
    
                deferred.await()
            } catch (e: Exception) {
    
    
                LogUtils.e("在 await 处捕获到 async的异常了")
            }
            LogUtils.e("后续代码继续执行")

        }
    }

It can be seen that the exception was indeed caught when awaiting after 2 seconds, and the app did not crash.

insert image description here


The correct posture of kotlin coroutine exception handling

This is just what I think is the right way to deal with it

First of all, when using coroutines, be sure to add coroutineexceptionhandler

This is a cover for the exceptions in the scope of the current coroutine, that is, the uncaught exceptions in the scope will be finally handed over to coroutineexceptionhandler for processing , so that at least your app will not crash

Let's look at the following code:

    /*异常处理*/
    private val exceptionHandler = CoroutineExceptionHandler {
    
     coroutineContext, throwable ->
        LogUtils.e("exceptionHandler:${
      
      throwable}")
    }

    fun testAsync() {
    
    
        viewModelScope.launch(exceptionHandler) {
    
    
            val deferred = async {
    
    
                LogUtils.e("准备抛出异常")
                delay(1000)
                throw Exception("async 抛出了一个异常")
            }
            /*加个延时 主要是验证异常是不是在await的时候抛出*/
            delay(2000)
            try {
    
    
                deferred.await()
            } catch (e: Exception) {
    
    
                LogUtils.e("在 await 处捕获到 async的异常了")
            }
            LogUtils.e("后续代码继续执行")
        }
    }

It is still the previous code, but a CoroutineExceptionHandler is added . This CoroutineExceptionHandler is not too verbose, and the official website document introduces it in more detail.

Then look at the running effect:

insert image description here

You can see
that the uncaught exception thrown by async is
handled by CoroutineExceptionHandler, so that even if there are some unhandled exceptions thrown in your coroutine block, it will not cause the App to crash

As for the difference between CoroutineScope and supervisorScope after adding CoroutineExceptionHandler, I won’t introduce it here. The official website introduces it more clearly.

To briefly summarize:

  • It is the safest way to try cath inside each coroutine. It is simple and rude. Although it is troublesome, it will not make mistakes, and it will be stable.
  • Regardless of whether there is try catch processing or not, be sure to add CoroutineExceptionHandler to the root scope, just in case
  • When using async await, pay attention to the problem of scope, so as to avoid unexpected results

Well, that's it for this article, I hope it can help you


If you think this article is helpful to you, please give it a thumbs up. It can help more developers. If there are any mistakes in the article, please correct me. For reprinting, please indicate that you are reposting from Yu Zhiqiang’s blog, thank you !

Guess you like

Origin blog.csdn.net/yuzhiqiang_1993/article/details/121049744