kotlin synergy async await's abnormal stepping pit and correct posture for exception handling

I believe you are very familiar with using Kotlin to do some asynchronous operations. In particular, combined with some components of Jetpack, it is very convenient for us to write asynchronous tasks in Android development.

However, when using a collaborative process, I feel that exception handling is a relatively time-consuming area to understand, because some small pits will still be encountered in the process of use. Here, record the pits encountered before.

Step on a pit for exception handling when using async await

Official document for exception handling of kotlin collaboration
Let's start with an official example:

As you can see, in the sample code, the exception capture of async enabled coroutines is performed when await is called. The print results are indeed captured. There is no problem.

According to the example code, we may use it naturally. For example, the following code is written in ViewModel:

    fun testAsync() {
        viewModelScope.launch {
            val deferred = async {
                LogUtils.e("Prepare to throw an exception")
                delay(1000)
                throw Exception("async An exception was thrown")
            }
            try {
                deferred.await()
            } catch (e: Exception) {
                LogUtils.e("stay await Captured at async It's abnormal")
            }
            LogUtils.e("Subsequent code execution continues")
        }
    }

According to the official sample code, async exceptions are thrown when await is called. If we try catch when calling await, we can catch the exception thrown by async, and the program will not crash.
Let's take a look at the actual operation:

Here, I called the above code when I entered the exception handling, and I found that the app crashed. Let's take a look at the printed log

We can see that we have caught exceptions when await. Why does the App crash?

If you look closely at the comments in the official example, you will find these two words root coroutine

Direct results:

When async is the root coprocessor, the exception encapsulated in the deferred object will be thrown when await is called.
If async is a subprocess, the exception will not be thrown when await is called, but will be thrown immediately.

This is why we try catch at await, but the program still crashes.
Because the exception thrown immediately is not handled, it can only crash.

It's just that the official documents do not clearly explain what the abnormality is as a sub process, so I stepped on the pit here before.

You may have questions here. No, look at the log. It is clear that the exception was thrown when await. How can you say that it was thrown immediately and how to prove it.
I can only say:

It's simple. Let's add a delay before await and just look at the log printing.
The code is as follows. As above, a delay is added directly.

    fun testAsync() {
        viewModelScope.launch {
            val deferred = async {
                LogUtils.e("Prepare to throw an exception")
                delay(1000)
                throw Exception("async An exception was thrown")
            }
            /*Adding a delay is mainly to verify whether the exception is thrown during await*/
            delay(2000)
            try {
                deferred.await()
            } catch (e: Exception) {
                LogUtils.e("stay await Captured at async It's abnormal")
            }
            LogUtils.e("Subsequent code execution continues")
        }
    }

Let's take a look at the operation. We can see that there is no chance to print the log of await's try catch. Why? Because the above code has been abnormal and the program has collapsed, there is no chance to wait 2 seconds to continue execution. The above code will print because it executes very fast, so it gives you the illusion that it is an exception thrown during await.

Next, let's verify whether the exception thrown when await is called as the top-level scope:
The same way, except that async becomes the top-level scope

    fun testTopAsync() {
        /*async for top-level scope*/
        val deferred = viewModelScope.async {
            LogUtils.e("Prepare to throw an exception")
            delay(1000)
            throw Exception("async An exception was thrown")
        }

        viewModelScope.launch {
            /*Adding a delay is mainly to verify whether the exception is thrown during await*/
            delay(2000)        
            try {
                deferred.await()
            } catch (e: Exception) {
                LogUtils.e("stay await Captured at async It's abnormal")
            }
            LogUtils.e("Subsequent code execution continues")

        }
    }

It can be seen that the exception was not caught until await 2 seconds later, and the app did not crash.

Correct posture of kotlin co process exception handling

This is just the right way to deal with it

First of all, when using a collaborative process, you must add coroutineexceptionhandler

This is to make a thorough explanation of the exceptions within the scope of the current collaboration, that is, the uncapped exceptions in the scope will eventually be handed over to coroutineexceptionhandler for processing, so as to at least ensure that your App will not crash

Let's look at the following code:

    /*exception handling*/
    private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        LogUtils.e("exceptionHandler:${throwable}")
    }

    fun testAsync() {
        viewModelScope.launch(exceptionHandler) {
            val deferred = async {
                LogUtils.e("Prepare to throw an exception")
                delay(1000)
                throw Exception("async An exception was thrown")
            }
            /*Adding a delay is mainly to verify whether the exception is thrown during await*/
            delay(2000)
            try {
                deferred.await()
            } catch (e: Exception) {
                LogUtils.e("stay await Captured at async It's abnormal")
            }
            LogUtils.e("Subsequent code execution continues")
        }
    }

It's the same as the previous code. It just adds a CoroutineExceptionHandler. This CoroutineExceptionHandler is not wordy. The official website documents are also introduced in detail.

Then take a look at the operation effect:

Can see
An exception thrown by async that is not caught is ignored
CoroutineExceptionHandler is handled, so that even if there are some exceptions thrown in your collaboration block that you forget to handle, it will not cause the App to crash

As for the difference between CoroutineScope and supervisorScope after adding CoroutineExceptionHandler, it is not introduced here, and it is quite clear on the official website.

To summarize briefly:

  • It is the safest way to try cath inside each cooperation process. It is simple and rough. Although it is troublesome, it will not make mistakes and is stable
  • No matter whether you do try catch or not, you must add CoroutineExceptionHandler to the root scope just in case
  • When using async await, you should pay attention to the scope to avoid different results than expected

Well, this article is like this. I hope it can help you

If you think this article is helpful to you, please move your finger to help more developers. If there are any mistakes in the article, please correct them. Please indicate the source of reprint Yu Zhiqiang's blog , thank you!

Keywords: Android kotlin

Added by henrygao on Sat, 30 Oct 2021 10:13:37 +0300