The exception handling of the coroutine is different from OKhttp and RxJava frameworks, because the exception handling of asynchronous code is often more troublesome, while the exception handling of the synchronous coroutine framework becomes easier to manage.

To fully understand the exceptions of coroutines, we need to understand the tree structure and structured concurrency of coroutines. Based on this, it is easy to understand how coroutines manage exceptions.

Coroutine trees and structured concurrency

In coroutine scope, you can create a coroutine, and you can create more coroutines within a coroutine, so it’s a tree. With this tree structure, the coroutine can easily control structured concurrency. The parent coroutine can control the life cycle of the child coroutine, and the child coroutine can inherit the coroutine context from the parent.

In code, you can explicitly create a coroutineScope via coroutineScope {}, which is the same scope builder for coroutines as runBlocking {}, which is often used in testing.

Cancel of the coroutine scope

With the help of coroutine scope management, we can easily control all coroutines under the coroutine scope. Once a coroutine scope is cancelled, all coroutines under the coroutine scope will be cancelled.

val job1 = scope.launch {... } val job2 = scope.launch {... } scope.cancel()Copy the code

As shown above, both Job1 and joB2 are cancelled after calling scope’s Cancel.

If you want to cancel a single coroutine, you can do so through the Job object that handles the coroutine.

Val job1 = scope.launch {... } val job2 = scope.launch {... } job1.cancel()Copy the code

As shown above, this cancels only the coroutine of Job1, leaving Job2 unaffected.

These are the two characteristics of coroutine structured concurrency:

  • Cancelling a coroutine scope cancels all subcoroutines in that coroutine scope
  • Cancelled subcoroutines do not affect other sibling coroutines

In Android development, we do not need to consider the cancel of coroutines in most scenarios. With the coroutine scope of ViewModelScope, LifecycleScope and MainScope, we can conveniently avoid memory leaks and terminate all subcoroutines at Cancel.

Cancel status of the coroutine

Cancel for a coroutine is similar to cancel for a thread. Once the coroutine has run (cpu-consuming code), it cancels only after it has run. When the coroutine calls cancel, its Job life is set to Canceled only after its execution has run.

If the execution of the coroutine must be cancelled in time, it is possible to do similar operations with threads to determine the state of the coroutine in time to control the execution of the code.

Therefore, it is recommended that developers use coroutines in a collaborative manner, that is, to keep track of the current life cycle of coroutines and avoid wasting computing resources.

Coroutines provide two ways to perform cooperative cancel:

  • Job. Or ensureActive isActive ()
  • yield

EnsureActive () is an encapsulated implementation of job. isActive that evaluates the current state of the coroutine before the code inside it executes.

Clean up the

In general, when a coroutine is canceled, some cleanup is needed. In this case, the code running in the coroutine can be wrapped ina try {} fininaly {} block, so that when the coroutine is canceled, the cleanup in the Fininaly block is performed. A CancellationException will be raised because it has already been cancelled, and you need to execute the suspension function in the Fininaly block to suspend it, which obviously contradicts the requirement. However, it is not impossible to do so if you need to suspend a cancelled coroutine, you can wrap the corresponding code in withContext(NonCancellable) {} and use the withContext function with the NonCancellable context, as shown below.

fun main() = runBlocking { val job = launch { try { repeat(1000) { i -> println("job: I'm sleeping $i ..." ) delay(500L)}} finally {withContext(NonCancellable) {println("job: I'm running finally") delay(1000L) Println ("job: And I've just delayed for 1 SEC because I'm non-cancellable")}}} delay(1300L) println("main: I'm tired of waiting!" CancelAndJoin () println("main: Now I can quit.")} job: I'm sleeping 0... job: I'm sleeping 1 ... job: I'm sleeping 2 ... main: I'm tired of waiting! job: I'm running finally job: And I've just delayed for 1 sec because I'm non-cancellable main: Now I can quit.Copy the code

The return value of the coroutine

The coroutine retrieves the return value in two ways:

  • The Job instance returned by launch can call the Join method (the Join function suspends the coroutine until it completes).
  • The Deferred instance returned by Async (a subclass of Job) can call the await method

If cancel is called after Join, the coroutine will be cancelled after completion, and if Cancel is followed by Join, the coroutine will also complete

Coroutine exception handling

When a coroutine in coroutine scope is abnormal, the abnormal flow is as follows:

  • The exception coroutine is cancelled
  • The exception is passed to its parent coroutine
  • Parent coroutine cancel (cancels all its children)
  • Propagate the exception further up the coroutine tree

This behavior actually conforms to the rules of coroutine structured concurrency, but in practice, this structured exception handling can make exception handling somewhat violent. In most cases, the business requirement is that exceptions do not affect the normal business process.

Structured concurrent exception handling

So the coroutine is overseeing a new concept called the Job, which is a subclass of the Job.

The container is designed to strangle anomalies in the coroutine and cut off its upward path. While the child coroutine is handling the container, the container is designed to handle the exception while it is not being broadcast.

The container is designed to be passed in as a parameter when the CoroutineScope is being created, or it is designed to create a custom CoroutineScope using the container’s container scope, so the container is only designed to be used in one of two ways.

  • supervisorScope{}
  • CoroutineScope(SupervisorJob())

It’s important to note, however, that if there’s an exception in the coroutine, whether it’s a Job or a Job, it’s going to be able to be handled and it’s going to crash.

The big mistake here is that you shouldn’t think that when you’re running the Job it’s going to be able to crash, it doesn’t matter what Job you’re running, it’s going to crash, it’s going to crash, the difference is it’s going to affect other coroutines, like this example here.

val coroutineScope = CoroutineScope(Job())
coroutineScope.launch {
    throw Exception("test")
}
coroutineScope.launch {
    Log.d("xys", "test")
}
Copy the code

The second coroutine isn’t going to be able to run when you’re handling the Job, but it’s going to be able to run when you change it to SupervisorJob() because the first coroutine is going to crash and it’s not going to affect the second coroutine.

So the container is designed to look for a special handling style in the structured concurrency, and it’s not designed to hide anomalies.

The largest container it is designed to be used in is the simultaneous handling of multiple coroutines so that anomalies on one coroutine won’t interfere with other normal coroutines. The CoroutineScope is also useful because you can cancel all coroutines associated with a coroutine when an exception occurs, as a unified treatment.

The coroutineScope is designed to be two-way in the direction of the abnormal flow, while the container is designed to be one-way.

The common MainScope is the container it is designed to be used in, so the subroutines in the MainScope will not affect each other.

Exception handling of coroutines

We said that an exception in a coroutine is always thrown, so how do we handle an exception inside a coroutine?

Launch: Exceptions launched by launch can be caught by a try catch, or by the coroutine wrapped extension function runCatching, which also uses a try catch inside.

Async: The exception handling of async is troublesome. We will talk about it in detail below.

First, when async is used to build the root coroutine (a coroutine managed directly by the coroutine scope), exceptions are not actively thrown, but instead are thrown when.await() is called.

Take a look at this example:

MainScope().launch {
    supervisorScope {
        val deferred = async {
            throw Exception("test")
        }
        try {
            deferred.await()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}
Copy the code

The container is designed to be handled in the same way that it is handled in the same way that it is handled in the same way that it is handled in the same way that it is handled in the same way that it is handled in the same way that it is handled in the same way that it is handled in the same way that it is handled.

When we run the code, we’ll see that the exception is not being caught, and that’s the difference we’re talking about between the Job and the Job.

Here’s another example:

MainScope().launch {
    try {
        async {
            throw Exception("test")
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
Copy the code

We’re going to remove the container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container container

In summary, async exceptions can only be handled in the container with a try catch.

CoroutineExceptionHandler

CoroutineExceptionHandler similar global exception handling in Android, when abnormal in coroutines tree, if there is no set CoroutineExceptionHandler, then thrown exception will continue to be passed until, But if the installed CoroutineExceptionHandler, can handle uncaught exception here, CoroutineExceptionHandler creation as shown below.

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    Log.d("xys", "---${coroutineContext}  ${throwable.printStackTrace()}")
}
Copy the code

Let’s look at the following example, set in the parent coroutines CoroutineExceptionHandler, when its child coroutines when an exception occurs, even if they don’t use the try catch, exception will be caught.

MainScope().launch(exceptionHandler) {
    async {
        throw Exception("test")
    }
}
Copy the code

But consider such a scene, make an exception occurs use CoroutineExceptionHandler coroutines, code as shown below.

MainScope().launch {
    async(exceptionHandler) {
        throw Exception("test")
    }
}
Copy the code

Unfortunately, this will not be able to catch exceptions, because CoroutineExceptionHandler belong to panic coroutines, itself can’t handle.

So CoroutineExceptionHandler use also have such restrictions, namely CoroutineExceptionHandler must be set in the anomalies of the father coroutines, its reason is the structured concurrent coroutines, abnormal will transfer to the parent coroutines in processing, so, There must be a parent coroutines set CoroutineExceptionHandler to take effect.

Note that the exception handling CoroutineExceptionHandler just coroutines “stubborn” finally, completely coroutines have Cancel, just give you a notice, abnormal coroutines, so here can only make a record of abnormal, can’t operate coroutines.

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit