preface

This article mainly includes the following contents: 1. Three scopes of coroutines and the propagation mode of exceptions 2. Two ways of catching coroutine exceptions and comparison 3. Elegant encapsulation of coroutine exceptions

If you find this article helpful, please help to like, thank you ~

How do exceptions to coroutines propagate?

Let’s start with the coroutine scope

Coroutines scope is divided into top class scope, synergy domain and master-slave scope, corresponding GlobalScope, coroutineScope, supervisorScope

Function analysis:

Description:

  • When abnormal c2-1 occurs, c2-1 ->C2->C2-2->C2->C1->C3 (including subcoroutines) ->C4
  • When an exception occurs at C3-1-1, c3-1-1 ->C3-1-1-1, and other parameters are not affected
  • When c3-1-1-1 is abnormal, c3-1-1-1 ->C3-1-1, and other parameters are not affected

For example

1. C1 has nothing to do with C2

GlobalScope.launch { / / coroutines C1
    GlobalScope.launch {/ / coroutines C2
        / /...}}Copy the code

C1 and C2 don’t affect each other, they’re completely independent

2.C2 and C3 are subcoroutines of C1. Exceptions to C2 and C3 cancel C1

GlobalScope.launch { / / coroutines C1
    coroutineScoope {
         launch{}/ / coroutines C2
         launch{}/ / coroutines C3}}Copy the code

3.C2 and C3 are subcoroutines of C1, and C2 and C3 exceptions do not cancel C1

GlobalScope.launch { / / coroutines C1
    supervisorScope {
         launch{}/ / coroutines C2
         launch{}/ / coroutines C3}}Copy the code

How to Catch Exceptions

1. What’s wrong with Try,Catch?

In Java and Kotlin, we usually just try and catch exceptions

fun main(a) {
    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
Copy the code

But when we launch a new coroutine in the try module, we get a surprise

fun main(a) {
    val topLevelScope = CoroutineScope(Job())
    topLevelScope.launch {
        try {
            launch {
                throw RuntimeException("RuntimeException in nested coroutine")}}catch (exception: Exception) {
            println("Handle $exception")
        }
    }
    Thread.sleep(100)}Copy the code

What you see is that the catch failed and the app crashed and we see that the try and catch cannot catch the exception of the subcoroutine

What happens to the exceptions that are not caught in the coroutine? One of the most innovative features of coroutines is structured concurrency. To make all the functionality of structured concurrency possible, the Job objects of CoroutineScope and the Job objects of Coroutines and child-Coroutines form a hierarchy of parent-child relationships. An exception that is not propagated (rather than rethrown) is “propagated in the work hierarchy.” This exception propagation will result in the failure of the parent Job, which in turn will result in the cancellation of all jobs at the child level.

The job hierarchy of the above example code looks something like this:



The exception of the subcoroutine is propagated to the Job of the coroutine (1), and then to the Job of topLevelScope (2).

The spread of the exception can be captured by CoroutineExceptionHandler, if not set, uncaught exception handling procedure of the calling thread, may lead to exit the application We see that there are two kinds of coroutines exception handling mechanism, it is also a coroutines exception handling more complex reasons

Summary 1

If the coroutine itself does not handle the exception itself using a try-catch clause, it does not rethrow the exception, so it cannot be handled through an external try-catch clause. Exceptions will be in “Job hierarchy”, can be set by the CoroutineExceptionHandler processing. If not, the thread’s uncaught exception handler is called.

2.CoroutineExceptionHandler

Now we know that a try-catch is useless if we launch a failed coroutine in a try block. Therefore, we need to configure a CoroutineExceptionHandler, we could pass the context to start coroutines generator. Since CoroutineExceptionHandler is a ContextElement, so we can pass on the promoter coroutines when pass it to launch:

fun main(a) {
  
    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
Copy the code

Can you find the program or crash why does it not work?

This is because to child coroutines set CoroutineExceptionHandler is of no effect, we must give top coroutines Settings, or initialize the Scope set is valid

// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...

// ...
topLevelScope.launch(coroutineExceptionHandler) {
// ...
Copy the code

Summary 2

In order to make the CoroutineExceptionHandler work, it must be set in the top CoroutineScope or coroutines.

3. Try to Catch and CoroutineExceptionHandler contrast

As described above, coroutines support two exceptional-handling mechanisms, so which should we choose?

CoroutineExceptionHandler official document provides some good answer:

“CoroutineExceptionHandler is used for global catch-all behavior of last resort. You can’t recover from the abnormal CoroutineExceptionHandler. When the handler is called, the coroutine is complete with the appropriate exception. Typically, handlers are used to log exceptions, display some kind of error message, and terminate and/or restart the application.

If exceptions need to be handled in a particular part of the code, it is recommended to use try/catch around the corresponding code inside the coroutine. This way, you can prevent the coroutine exception from completing (the exception is now caught), retry the operation and/or take any other action:”

Summary 3

If you want to retry the operation or do something else before the coroutine completes, use try/catch. Remember that by catching an exception directly in a coroutine, the exception is not propagated in the Job hierarchy and does not take advantage of the cancellation capabilities of structured concurrency. And use CoroutineExceptionHandler processing logic should be after completion of the coroutines. It can be seen that most of the time we should use CoroutineExceptionHandler

4.launch{} vs async{}

Our examples above use launch to launch coroutine exceptions, but launch and async cohandling are completely different. Let’s look at an example

fun main(a) {

    val topLevelScope = CoroutineScope(SupervisorJob())

    topLevelScope.async {
        throw RuntimeException("RuntimeException in async coroutine")
    }

    Thread.sleep(100)}// No output
Copy the code

Why won’t an exception be thrown here? Let’s first understand the difference between launch and async

The return type for coroutines starting from launch is Job, which is just a representation of the coroutine and does not return a value. If we need some result of the coroutine, we must use async, which returns Deferred, which is a special Job, and also holds a result value. If the asynchronous coroutine fails, wrap the exception in the Deferred return type and rethrow it when we call the suspend function.await () to retrieve its result value.

Therefore, we can enclose calls to.await () with a try-catch clause.

fun main(a) {

    val topLevelScope = CoroutineScope(SupervisorJob())

    val deferredResult = topLevelScope.async {
        throw RuntimeException("RuntimeException in async coroutine")
    }

    topLevelScope.launch {
        try {
            deferredResult.await()
        } catch (exception: Exception) {
            println("Handle $exception in try/catch")
        }
    }

    Thread.sleep(100)}/ / output
// Handle java.lang.RuntimeException: RuntimeException in async coroutine in try/catch
Copy the code

Note: If the async coroutine is a top-level coroutine, the exception is wrapped in the Deferred and thrown until the await is called. Otherwise, the exception will spread to the Job hierarchy and handled by CoroutineExceptionHandler, even passed to the thread uncaught exception handler, even not the invocation. Await (), as shown in the example below:

fun main(a) {
  
    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
Copy the code

Summary 4

Exceptions not caught in launch and Async coroutines are immediately propagated in the job hierarchy. However, if the top Coroutine from launch, the launch of the exception will be handled by CoroutineExceptionHandler or passed to the thread uncaught exception handler. If the top-level coroutine is started as async, the exception is wrapped in the Deferred return type and rethrown on the call to.await ().

5. CoroutineScope exception handling feature

As an example at the beginning of this article, a failed coroutine propagates its exception into the Job hierarchy instead of rethrowing the exception, and therefore an external try-catch is invalid. However, something interesting happens when we surround the failing coroutine with the coroutineScope {} scoped function:

fun main(a) {
    
  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
Copy the code

Now, we can handle exceptions using the try-catch clause. As you can see, the scoping function coroutineScope {} rethrows an exception to its failed child, rather than propagating it into the Job hierarchy.

CoroutineScope {} is primarily used in suspend functions to achieve “parallel decomposition.” These suspend functions will rethrow the exception of their failed coroutines, so we can set up the exception handling logic accordingly.

5. Summary of 5

The range function coroutineScope {} rethrows the exception of its failed subcoroutine, rather than propagating it into the Job hierarchy, which allows us to use a try-catch to handle the exception of a failed coroutine

SupervisorScope is the exception handling feature

SupervisorScope {} we’re going to add a new, separate nested scope to the Job hierarchy, and we’re going to use the SupervisorJob as its Job.

The code is as follows:

fun main(a) {

    val topLevelScope = CoroutineScope(Job())

    topLevelScope.launch {
        val job1 = launch {
            println("starting Coroutine 1")
        }

        supervisorScope {
            val job2 = launch {
                println("starting Coroutine 2")}val job3 = launch {
                println("starting Coroutine 3")
            }
        }
    }

    Thread.sleep(100)}Copy the code

Now, the important thing to know about exception handling here is that the supervisorScope is a new, independent subdomain that must handle exceptions on its own. It does not rethrow the exception of a failed coroutine as coroutineScope does, nor does it propagate the exception to its parent, the topLevelScope job.

The other important thing to understand is that exceptions only propagate upwards until they reach the top range, or SupervisorJob. This means that joB2 and Job3 are now top-level coroutines. This also means that we can add CoroutineExceptionHandler for them

fun main(a) {

    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
Copy the code

The coroutines launched directly in the supervisorScope are top-level coroutines, This also means that async coroutines now wrap their exceptions in their Deferred objects and are only rethrown when calling.await () which is why async in viewModelScope needs to call await to throw an exception

Summary of 6

The supervisorScope {} function supervisorScope {} is designed to create a new, separate sub-scope in the Job hierarchy, and it is intended to be the scope’s ‘Job’. This new scope does not propagate its exceptions in the “Job hierarchy,” so it must handle its own exceptions. Coroutines launched directly from the supervisorScope are top-level coroutines. Top coroutines and son coroutines in the use of launch () or async () startup behavior is different, in addition, you can also install CoroutineExceptionHandlers in them.

Coroutine exception handling encapsulation

As stated above, in most of the time, CoroutineExceptionHandler is a better choice

As we all know, the biggest advantages of coroutines synchronization methods can be used to write asynchronous code, 1 CoroutineExceptionHandler has the following shortcomings. 2. Creating a new local variable every time you use it is not elegant

We can to encapsulate CoroutineExceptionHandler using kotlin extension function, achieve similar RxJava call effect The last call effect is as follows

fun fetch(a) {
        viewModelScope.rxLaunch<String> {
            onRequest = {
                // Network request
                resposity.getData()
            }
            onSuccess = {
                // Successful callback
            }
            onError = {
                // Fail callback}}}Copy the code

Code implementation

The main use of Kotlin extension function and DSL syntax, encapsulated coroutine exception handling, similar to the effect of RxJava call

fun <T> CoroutineScope.rxLaunch(init: CoroutineBuilder<T>. () - >Unit) {
    val result = CoroutineBuilder<T>().apply(init)
    valcoroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> result.onError? .invoke(exception) } launch(coroutineExceptionHandler) {valres: T? = result.onRequest? .invoke() res? .let { result.onSuccess? .invoke(it) } } }class CoroutineBuilder<T> {
    var onRequest: (suspend () -> T)? = null
    var onSuccess: ((T) -> Unit)? = null
    var onError: ((Throwable) -> Unit)? = null
}
Copy the code

As above is a simple packaging, which can realize the above the goals of the demonstration effect Will ask for instructions, success, failure classification, structure more clear, don’t need to write CoroutineExceptionHandler local variables at the same time, more elegant and concise

The resources

About Kotlin Coroutines, An analysis of the exception handling mechanism of The Android-Kotlin coroutine why-exception-handling-with-kotlin-coroutines-is-so-hard-and-how-to-successfully-master-it

This article is participating in the “Nuggets 2021 Spring Recruiting activity”, click to see the details of the activity