Key words: Kotlin coroutine exception handling
Exception handling in asynchronous code is often a pain in the neck, and coroutines are once again showing their power.
1. The introduction
We mentioned an example in the previous article:
typealias Callback = (User) -> Unit
fun getUser(callback: Callback){... }Copy the code
We usually define a callback interface for asynchronous data requests, which we can easily convert into a coroutine interface:
suspend fun getUserCoroutine(a) = suspendCoroutine<User> {
continuation ->
getUser {
continuation.resume(it)
}
}
Copy the code
And ultimately hand over to a button click event or other event to trigger the asynchronous request:
getUserBtn.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
userNameView.text = getUserCoroutine().name
}
}
Copy the code
So the problem is, since it’s a request, there’s always a failure, and we don’t have any error handling here, so let’s refine this example.
2. Add the exception processing logic
First we add the exception callback interface function:
interface Callback<T> {
fun onSuccess(value: T)
fun onError(t: Throwable)
}
Copy the code
Let’s change our getUserCoroutine:
suspend fun getUserCoroutine(a) = suspendCoroutine<User> { continuation ->
getUser(object : Callback<User> {
override fun onSuccess(value: User) {
continuation.resume(value)
}
override fun onError(t: Throwable) {
continuation.resumeWithException(t)
}
})
}
Copy the code
As you can see, we seem to have completely converted the Callback into a Continuation, and when we call it we just need to:
GlobalScope.launch(Dispatchers.Main) {
try {
userNameView.text = getUserCoroutine().name
} catch (e: Exception) {
userNameView.text = "Get User Error: $e"}}Copy the code
Yes, you read that correctly, an asynchronous request exception, we only need to catch in our code, the benefit of this is that the entire process of the request exception can be in a try… catch … Then we can say that we have really turned asynchronous code into synchronous writing.
If you’ve been handling this logic with RxJava, your request interface might look something like this:
fun getUserObservable(a): Single<User> {
return Single.create<User> { emitter ->
getUser(object : Callback<User> {
override fun onSuccess(value: User) {
emitter.onSuccess(value)
}
override fun onError(t: Throwable) {
emitter.onError(t)
}
})
}
}
Copy the code
The call looks something like this:
getUserObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe ({ user ->
userNameView.text = user.name
}, {
userNameView.text = "Get User Error: $it"
})
Copy the code
You can easily see that RxJava is doing the same thing here as the coroutine, but in a more natural way.
You may already be familiar with RxJava and feel comfortable with it, but RxJava code is much more complex and confusing than coroutines, as we will continue to illustrate with examples throughout the rest of this article.
3. Global exception handling
Both threads and RxJava have ways to handle exceptions globally, such as:
fun main(a) {
Thread.setDefaultUncaughtExceptionHandler {t: Thread, e: Throwable ->
//handle exception here
println("Thread '${t.name}' throws an exception with message '${e.message}'")}throw ArithmeticException("Hey!")}Copy the code
We can set global exception catching for threads, and we can also set global exception catching for RxJava:
RxJavaPlugins.setErrorHandler(e -> {
//handle exception here
println("Throws an exception with message '${e.message}'")});Copy the code
Coroutines can obviously do the same. Similar to Thread through. SetUncaughtExceptionHandler set an exception capture device for Thread, we can also for each collaborators CoroutineExceptionHandler alone setting, cheng coroutines within an uncaught exception can be used to capture:
private suspend fun main(a){
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
log("Throws an exception with message: ${throwable.message}")
}
log(1)
GlobalScope.launch(exceptionHandler) {
throw ArithmeticException("Hey!")
}.join()
log(2)}Copy the code
Running result:
19:06:35:087 [main] 1
19:06:35:208 [DefaultDispatcher-worker-1 @coroutine#1] Throws an exception with message: Hey!
19:06:35:211 [DefaultDispatcher-worker-1 @coroutine2 # 1]
Copy the code
CoroutineExceptionHandler unexpectedly is a context, this context of coroutines is the existence of the soul, it’s not at all surprising.
Of course, this is not a global exception catch, because it can only catch the exception that is not caught in the corresponding coroutine. If you want to truly global catch, we can define our own catch class on the Jvm:
class GlobalCoroutineExceptionHandler: CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*> = CoroutineExceptionHandler
override fun handleException(context: CoroutineContext, exception: Throwable) {
println("Coroutine exception: $exception")}}Copy the code
Then created in the classpath meta-inf/services/kotlinx coroutines. CoroutineExceptionHandler, File name is the full name of the class CoroutineExceptionHandler actually, all the file content can we write the implementation class name of the class:
com.bennyhuo.coroutines.sample2.exceptions.GlobalCoroutineExceptionHandler
Copy the code
The exception that is not caught in the coroutine will eventually be handed over to it.
On the Jvm CoroutineExceptionHandler global configuration, is essentially the application of ServiceLoader, before we’re telling Dispatchers. The Main mentioned, Its implementation on the Jvm is also loaded through the ServiceLoader.
Need clear is that through the async start coroutines appear an uncaught exception will ignore CoroutineExceptionHandler, this with the launch of the design is different.
4. Exception propagation
Exception propagation also involves the concept of coroutineScope. For example, we always start coroutines with GlobalScope, which means it is a separate top-level coroutineScope, and coroutineScope {… SupervisorScope {supervisorScope} }.
- Coroutines launched via GlobeScope launch a coroutine scope separately, and internal subcoroutines follow default scoping rules. Coroutines launched with GlobeScope are “in a league of their own.”
- A coroutineScope is a context creation scope that inherits an external Job. Cancellations within it are propagated in both ways, and exceptions not caught by the child coroutine are passed up to the parent coroutine. It is more suitable for a series of equivalent coroutines to complete a job concurrently, and if any of the subcoroutines exits unexpectedly, the whole will exit, which is simply “all lose”. This is also the default scope for subcoroutines that are rebooted within a coroutine.
- SupervisorScope is designed to inherit the context of the outer scope, but its internal cancellations are propagated unidirectional, with the parent to the subcoroutine and vice versa. This means that exceptions to the subcoroutine do not affect the parent or sibling. It is more suitable for independent, unrelated tasks, where a problem with one task does not affect the rest of the task, and in a simple way, it is “self-imposed”. For example, UI, when I click a button and an exception occurs, it does not affect the status bar of the phone. The supervisorScope is designed to run on a subcoroutine, and it is designed to run on a subcoroutine. It is designed to run on a subcoroutine, and it is designed to run on a subcoroutine. It is designed to run on a subcoroutine, and it is designed to run on a subcoroutine.
This is a bit abstract, so let’s take some examples:
suspend fun main(a) {
log(1)
try {
coroutineScope { / / 1.
log(2)
launch { / / 2.
log(3)
launch { / / 3.
log(4)
delay(100)
throw ArithmeticException("Hey!!")
}
log(5)
}
log(6)
val job = launch { / / 4.
log(7)
delay(1000)}try {
log(8)
job.join()
log("9")}catch (e: Exception) {
log("10. $e")
}
}
log(11)}catch (e: Exception) {
log("12. $e")
}
log(13)}Copy the code
This example is a little more complicated, but it’s not hard to understand. We start two coroutines ② and ④ in a coroutineScope, and we start a subcoroutine ③ in ②. The coroutine created directly by scope is marked ①. So what happens in the middle throw? Let’s look at the output first:
11:37:36:208 [main] 1
11:37:36:255 [main] 2
11:37:36:325 [DefaultDispatcher-worker-1] 3
11:37:36:325 [DefaultDispatcher-worker-1] 5
11:37:36:326 [DefaultDispatcher-worker-3] 4
11:37:36:331 [main] 6
11:37:36:336 [DefaultDispatcher-worker-1] 7
11:37:36:336 [main] 8
11:37:36:441 [DefaultDispatcher-worker-1] 10. kotlinx.coroutines.JobCancellationException: ScopeCoroutine is cancelling; job=ScopeCoroutine{Cancelling}@2bc92d2f
11:37:36:445 [DefaultDispatcher-worker-1] 12. java.lang.ArithmeticException: Hey!!
11:37:36:445 [DefaultDispatcher-worker-1] 13
Copy the code
Notice two places, one is 10, we call join and receive a CancellationException. The suspend method that supports the cancellation operation in the coroutine throws a CancellationException when it is cancelled, which is similar to the thread’s response to InterruptException. This means that the coroutine where the join call was made has been cancelled. So what is the cancellation?
Coroutines (3) original throws an uncaught exception, entered the abnormal state of complete, between it and the father coroutines (2) to follow the rules of the default scope, so (3) will notify its parent coroutines or (2) cancelled, (2) according to the rules of scope to inform the father coroutines (1) the whole scope to cancel, this is a transmission of a bottom-up, In this case, the call to job.join will throw an exception, which is the result at 10. For those of you who don’t quite understand this operation, consider what we said about coroutineScope, which internally initiates coroutines that are “mutually destruct.” In fact, since the parent coroutine (1) is cancelled, the coroutine (4) is not immune. If you are interested, you can also catch the delay in (4), and you will also get a cancellation exception.
If the coroutineScope ends with an exception, then we can try it directly. If the coroutineScope ends with an exception, we can try it directly. catch … To catch this exception, again indicating that the coroutine handles asynchronous exceptions into synchronous code logic.
So if we change coroutineScope to supervisorScope, and nothing else, what is the result?
11:52:48:632 [main] 1
11:52:48:694 [main] 2
11:52:48:875 [main] 6
11:52:48:892 [DefaultDispatcher-worker-1 @coroutine3 # 1]
11:52:48:895 [DefaultDispatcher-worker-1 @coroutine5 # 1]
11:52:48:900 [DefaultDispatcher-worker-3 @coroutine4 # 3]
11:52:48:905 [DefaultDispatcher-worker-2 @coroutine7 # 2]
11:52:48:907 [main] 8
Exception in thread "DefaultDispatcher-worker-3 @coroutine#3" java.lang.ArithmeticException: Hey!!
at com.bennyhuo.coroutines.sample2.exceptions.ScopesKt$main$2The $1The $1.invokeSuspend(Scopes.kt:17)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
11:52:49:915 [DefaultDispatcher-worker-3 @coroutine9 # 2]
11:52:49:915 [DefaultDispatcher-worker-3 @coroutine11 # 2]
11:52:49:915 [DefaultDispatcher-worker-3 @coroutine13 # 2]
Copy the code
We can see that there is no essential difference between the outputs of 1-8. The difference in order is caused by the thread scheduling before and after, and does not affect the semantics of coroutines. The scope is designed to run on its supervisorScope. It is designed to run on its supervisorScope, and it is designed to run on its supervisorScope. It is designed to run on its supervisorScope, and it is designed to run on its supervisorScope.
This example is a bit to make some changes, for the (2) and (3) adding a CoroutineExceptionHandler, can prove we mentioned a different conclusion:
First, we define a CoroutineExceptionHandler, we obtain the anomalies corresponding coroutines from context name:
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
log("${coroutineContext[CoroutineName]} $throwable")}Copy the code
Then, based on the previous example we add CoroutineExceptionHandler and name for the (2) and (3) :
. supervisorScope {/ / 1.
log(2)
launch(exceptionHandler + CoroutineName("2.")) { / / 2.
log(3)
launch(exceptionHandler + CoroutineName("3.")) { / / 3.
log(4)...Copy the code
Run this program again, and the results are interesting:
. 07:30:11:519] [DefaultDispatcher - worker - 1 CoroutineName (2) Java. Lang. ArithmeticException: Hey!!!!! .Copy the code
We found that trigger CoroutineExceptionHandler unexpectedly is coroutines (2), an accident? The supervisorScope subcoroutine is designed to run on its supervisorScope subcoroutine, and it is designed to follow the default coroutineScope rule if it is not specified. An uncaught exception is passed to the parent coroutine and cancelled.
What Scope to use should be determined according to the actual situation, and I give some suggestions:
- GlobalScope is suitable for situations where there is no coroutine scope but you need to start a coroutine
- In cases where there is already a coroutine scope (such as inside a coroutine launched via GlobalScope), start directly with a coroutine initiator
- SupervisorScope is used when subcoroutines are required to be independent of each other
- For coroutines created through the standard library API, such coroutines are relatively low level and are not supported by the concepts of jobs, scopes, etc., as we mentioned earlier in the case of Suspend Main. In this case, coroutineScope is preferred. Further, try not to use the standard library API directly unless you are familiar with Kotlin’s coroutine mechanism.
Of course, for the possible exception of the situation, please try to do the exception handling, do not complicate the problem.
5. Join and await
Async, actor, and produce are also commonly used for launching coroutines. Actors behave similarly to launch and are thrown as handled exceptions when uncaught, as in the previous example. Async and produce are primarily used to output results, and their internal exceptions are only thrown when their results are consumed externally. The initiators of these two sets of coroutines, which you can also think of as “consumers” and “producers,” are thrown immediately by the consumer and only when the result is consumed by the producer.
The Actor and Produce apis are currently in a delicate position and may be deprecated or may provide alternatives that are not recommended, but we won’t go into detail here.
So what are the consumption outcomes? For async, this is await, for example:
suspend fun main(a) {
val deferred = GlobalScope.async<Int> {
throw ArithmeticException()
}
try {
val value = deferred.await()
log("1. $value")}catch (e: Exception) {
log("2. $e")}}Copy the code
This makes logical sense. When we call await, we expect deferred to give us an appropriate result, but it has no way to do so because it throws an exception.
13:25:14:693 [main] 2. java.lang.ArithmeticException
Copy the code
Our own implementation of getUserCoroutine is in a similar situation, and if the request throws an exception when fetching results, we get an exception instead of the normal result. In contrast, join is much more interesting. It only cares if the execution is complete, and it doesn’t care what it is, so if we replace join here:
suspend fun main(a) {
val deferred = GlobalScope.async<Int> {
throw ArithmeticException()
}
try {
deferred.join()
log(1)}catch (e: Exception) {
log("2. $e")}}Copy the code
We will find that the exception has been swallowed!
13:26:15:034 [main] 1
Copy the code
If we use launch instead of async in our example, there will still be no exceptions thrown at the join, and again, it only cares if it’s done, it doesn’t care how it’s done. The difference is that the uncaught exceptions in launch are handled differently than async. The launch is thrown directly to the parent coroutine, and the parent coroutine does not respond when it is not in the launch scope or when it is in the scope. Then handed over to the context specified CoroutineExceptionHandler processing, if not specified, then passed on to the global CoroutineExceptionHandler etc., while async and await to consume.
Async does not need to call await to get the exception. It will also trigger the cancellation logic of the parent coroutine in the coroutineScope. I’d like to draw your attention to this.
6. Summary
In this article we talked about exception handling for coroutines. This is a little bit complicated, but if you look carefully there are three main lines:
- Coroutine internal exception handling process: Will launch in an uncaught exception occurs when the father try to trigger for coroutines cancel, can cancel to look at the definition of the scope, if cancel the success, then passed to the parent coroutines, otherwise the context is passed to the startup configuration CoroutineExceptionHandler, if there is no configuration, Will find global (JVM) CoroutineExceptionHandler for processing, if still not, then the exception to the current thread UncaughtExceptionHandler processing; Async will also attempt to cancel the parent coroutine when an uncaught exception occurs, but will not do any subsequent exception handling whether or not it succeeds until the user actively calls the await and throws the exception.
- Exception propagation in scope: In coroutineScope, a coroutine exception triggers cancellation of the parent coroutine, which cancles the entire coroutineScope. If the coroutineScope overall capture, can also catch the exception, the so-called “all lose”; The subroutine is designed to run on its supervisorScope, and no subcoroutine exceptions are passed up.
- The difference between join and await: join only cares about whether the coroutine completes, await only cares about the result of running, so join does not throw an exception if the coroutine has an exception, while await does; Considering the scope problem, if the coroutine throws an exception, it may cause the cancellation of the parent coroutine. Therefore, although the exception of the coroutine itself will not be thrown when the join call is called, it will throw a cancellation exception if the coroutine in which the join call is called is cancelled, which needs to be noted.
If you can understand these three points clearly, then the exception handling of coroutines can be said to be very clear. Cancellation is mentioned in this article because of exception propagation, but it is not discussed in detail. We will do an article on cancellation output later to help you understand.
Additional instructions
When the parent coroutine is cancelled, there is a bug in join that causes the cancellation exception not to be thrown. I found this problem when preparing this article. It has been submitted to the official and fixed, and it is expected to be released in 1.2.1. Thrown CancellationException when join on a crashed Job.
Of course, this bug has little impact on the generated environment, so don’t worry about it.
Welcome to Kotlin Chinese community!
Chinese website: www.kotlincn.net/
Chinese official blog: www.kotliner.cn/
Official account: Kotlin
Zhihu column: Kotlin
CSDN: Kotlin Chinese community
Nuggets: Kotlin Chinese Community
Kotlin Chinese Community