Coroutines in Kotlin have become a common way in network requests. In addition to normal requests, we also need to deal with exceptions in requests. This article will deal with exceptions in coroutines in the following parts:

\qquad\qquad 1.2 When a try-catch is invalid? \qquad\qquad 1.3 What is structured concurrency of coroutines? \ qquad 2, CoroutineExceptionHandler \ qquad \ qquad CoroutineExceptionHandler introduction \ qquad \ qquad 2.2 2.1 The use of the CoroutineExceptionHandler \ qquad \ qquad 2.3 CoroutineExceptionHandler less than three, SupervisorScope + async \ \ qquad qquad four, conclusion

In addition, network request exception encapsulation can refer to the specific project

A try-catch is used to catch exceptions

1.1 Try-catch basic use

In general, exceptions can be handled using a try-catch block, where the request code is written in a try and the catch catches the exception.

Take a common request as an example:

Api interface:

interface ProjectApi {
    @GET("project/tree/json")
    suspend fun loadProjectTree(): BaseResp<List<ProjectTree>>

    @GET("project/tree/jsonError")
    suspend fun loadProjectTreeError(): BaseResp<List<ProjectTree>>
}
Copy the code

The Api lists two interfaces, loadProjectTree() as the interface that can successfully request, and loadProjectTreeError() deliberately adds ‘Error’ to the path to simulate the state of request failure.

Specific call:

suspend fun loadProjectTree() {
        try {
            val result = service.loadProjectTree()
            val errorResult = service.loadProjectTreeError()
            Log.d(TAG, "loadProjectTree: $result")
            Log.d(TAG, "loadProjectTree errorResult: $errorResult")}catch (e: Exception) {
            Log.d(TAG, "loadProjectTree: Exception " + e.message)
            e.printStackTrace()
        }
    }
Copy the code

Let’s look at the result of the call:

loadProjectTree: Exception HTTP 404 Not Found

Because path was intentionally miswritten in loadProjectTreeError, the execution process naturally went into a catch and reported a 404 error. LoadProjectTree and loadProjectTreeError are both successful and failed, but when the two interfaces are placed in the same trycatch block, if either of them fails, the other request will not be executed, even if it succeeds.

If we want our interfaces to be unaffected by each other, we need to create separate try-catch blocks for each interface. As follows:

 suspend fun loadProjectTree() {
        try {
            val errorResult = service.loadProjectTreeError()
            Log.d(TAG, "loadProjectTree errorResult: $errorResult")}catch (e: Exception) {
            Log.d(TAG, "loadProjectTree: error Exception " + e.message)
            e.printStackTrace()
        }

        try {
            val result = service.loadProjectTree()
            Log.d(TAG, "loadProjectTree: $result")}catch (e: Exception) {
            Log.d(TAG, "loadProjectTree: Exception " + e.message)
            e.printStackTrace()
        }
    }
Copy the code

LoadProjectTreeError and loadProjectTree are caught using try-catch blocks, respectively, and the results are as expected. The interface requests do not affect each other:

loadProjectTree: error Exception HTTP 404 Not Found

loadProjectTree: com.fuusy.common.network.BaseResp@57e153d

This is the usual way to handle coroutine exceptions, but in some cases it is possible to fail to catch an exception with a try-catch.

1.2 When is a try-catch invalid?

Normally, only code blocks in a try-catch block have exceptions, which are caught in the catch. But there are special cases of exceptions in coroutines.

For example, if a failed child coroutine is opened in a coroutine, it cannot be caught. Take an example of the interface above:

     fun loadProjectTree(a) {
        viewModelScope.launch() {
            try {
                / / coroutines
                launch {
                    // Failed interface
                    service.loadProjectTreeError()
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }

        }
    }
Copy the code

We create a child coroutine in a try-catch block that calls an interface that will fail 100% of the time. We expect to catch the exception in the catch, but when we actually run it, we find that the App crashes and exits. This also validates that try-catch is invalid.

So why does a try-catch fail if you start a failing child coroutine in a coroutine? This has to mention a new knowledge point, coroutine structured concurrency.

1.3 What is structured concurrency of coroutines?

In Kotlin’s coroutines, the global GlobalScope is a scope, and each coroutine is itself a scope. The newly created coroutine has a cascading relationship with its parent scope, that is, a parent-child relationship hierarchy. And this cascading relationship mainly lies in:

  1. The life of the parent scope lasts until all child scopes are executed;

  2. When the end parent scope ends, all of its child scopes end.

  3. Exceptions that are not caught by the child scope are not rethrown, but are passed level by level to the parent scope, which causes the parent scope to fail and all requests for the child scope to be cancelled.

The three points above are the structured concurrency features of coroutines.

Now that we know what structured concurrency is for coroutines, we can get back to try-catch why does it fail if you start a failing child coroutine in a coroutine? On the question of. Obviously, point 3 above is the answer to this problem. Exceptions that are not caught in the child coroutine are not rethrown, but propagated up the parent hierarchy, which causes the parent Job to fail.

In this case, we should use a new method of handling exceptions: CoroutineExceptionHandler

Second, the CoroutineExceptionHandler global catch exceptions

Besides try-catch coroutines the second method is to use CoroutineExceptionHandler handle exceptions.

2.1 CoroutineExceptionHandler is introduced

First let’s look at what is CoroutineExceptionHandler?

CoroutineExceptionHandler is used for global “catch all” finally a mechanism of behavior. You can’t recover from abnormal in CoroutineExceptionHandler. When the handler is called, the coroutine has already completed the corresponding exception. Typically, this handler is used to log exceptions, display some kind of error message, and terminate and/or restart the application.

Explanation for CoroutineExceptionHandler, this is the official document at first when I for this explanation is no more able to read and carefully wanted to think, behind CoroutineExceptionHandler as the way to a global catch exceptions, is because of the existence of coroutines structural characteristics of the concurrent Child after level 1 level of transfer of abnormal scope, finally by CoroutineExceptionHandler for processing. Because pass to CoroutineExceptionHandler scope has reached the top, in this case, the child coroutines has ended. That is to say in CoroutineExceptionHandler is invoked, all child coroutines have already completed the corresponding anomaly.

The use of 2.2 CoroutineExceptionHandler

First, we created an exceptionHandler in our ViewModel,

    private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        Log.d(TAG, "CoroutineExceptionHandler exception : ${exception.message}")}Copy the code

We then attach exceptionHandler to the viewModelScope.

    fun loadProjectTree() {
        viewModelScope.launch(exceptionHandler) {
            // Failed interface
            service.loadProjectTreeError()
        }
    }
Copy the code

According to the structural characteristics of the concurrent coroutines, when the root coroutines through launch {} starts, exceptions will be passed to have additional CoroutineExceptionHandler.

2.3 CoroutineExceptionHandler deficiencies

Don’t use try-catch coroutines, CoroutineExceptionHandler as the mechanism of the global catch exceptions, finally in CoroutineExceptionHandler exception will handle. But there are two caveats:

  1. Because there is no try-catch to catch exceptions, the exception is propagated upwards until it reaches the RootScope or SupervisorJob, and because of the structured concurrency nature of the coroutine, when an exception is propagated upwards, the parent coroutine will fail, as will the cascading children and siblings of the parent coroutine.

If you want to ask for more than one interface in parallel and need each other do not influence the execution of the tasks, is any one interface is unusual, will continue to perform other tasks, so CoroutineExceptionHandler is not a good choice. The container it is handling is designed to be seen in a more suitable light.

  1. CoroutineExceptionHandler role in global catch exceptions, CoroutineExceptionHandler couldn't exception handling in the particular section of the code, for example, to a single interface failure, unable to retry or other specific operation after the exception.

Try-catch is better if you want to do exception handling in a particular part.

Third, SupervisorScope + async

A defect in the above mentioned CoroutineExceptionHandler 2.3: child coroutines abnormal, coroutines coroutines father and his brothers also failed to perform will follow.

The kotlin coroutine is designed to solve this problem by introducing another coroutine scope: the SupervisorScope

When this scope is combined with async to start a coroutine, an exception occurs in the child coroutine, which does not affect its parent coroutine or its sibling coroutine. Therefore, it is more suitable for the execution of multiple parallel tasks.

Here’s an example:

viewModelScope.launch() {
            supervisorScope {
                try {
                    // If the divisor is 0, an exception is thrown
                    val deferredFail = async { 2 / 0 }
                    / / success
                    val deferred = async {
                        2 / 1
                        Log.d(TAG, "loadProjectTree: 2/1 ")
                    }
                    deferredFail.await()
                    deferred.await()

                } catch (e: Exception) {
                    Log.d(TAG, "loadProjectTree:Exception ${e.message} ")}}}Copy the code

The container is designed to handle two division operations, one of which is zero, and the other is designed to simulate a successful state. The container is designed to run with the following result:

As a result, a divisor of 0 throws an exception and the other sibling coroutine runs normally

Four, conclusion

  • To handle exceptions in a specific part of your code, use a try-catch;
  • Abnormal global catch exceptions, and one of the tasks, not perform other tasks, can use CoroutineExceptionHandler, save the resource consumption;
  • It is designed to be able to run in parallel mode when one of the tasks fails and the other tasks run normally. It is designed to be able to run in parallel mode when the other tasks fail.

References:

Why exception handling with Kotlin Coroutines is so hard and how to successfully master it!