Originally written by Manuel Vivo
Exceptions in Coroutines
Translator: Bingxin said
This article is the third in a series on coroutine cancellations and exceptions, which follows:
Coroutines: First things first
How to gracefully handle coroutine cancellations?
Before reading this article, I strongly recommend reviewing the previous two articles. If you really don’t have time, at least read the first article.
Let’s begin the text.
As developers, we often spend a lot of time perfecting our apps. However, it is equally important to provide as good a user experience as possible when an exception occurs that causes the application to not perform as expected. On the one hand, application Crash is a terrible experience for users. On the other hand, it is essential to provide correct information when a user operation fails.
Elegant exception handling is important to users. In this article, I’ll show you how exceptions are propagated in coroutines and how you can use various methods to control the propagation of exceptions.
If you prefer the video, you can watch Florina Muntenescu and I speak at KotlinConf’19 at the following address:
To help you understand the rest of this article, I recommend reading the First article in the series, Coroutines: First Things First
Coroutines suddenly fail? How to do? π±
When an exception occurs in a coroutine, it propagates the exception to its parent coroutine, which does several things:
- Cancel other subcoroutines
- To cancel your
- Propagates the exception to its parent helper
The exception will eventually propagate to the root of the inheritance structure. All coroutines created through this CoroutineScope will be cancelled.
In some scenarios, such exception propagation is appropriate. However, there are some scenarios that don’t fit.
Imagine a UI-specific CoroutineScope that handles user interactions. If one of its child coroutines throws an exception, the UI Scope is cancelled. Because the canceled scope cannot start any more coroutines, the entire UI component cannot respond to user interactions.
What if you don’t want this? Perhaps, when creating a coroutine scoped CoroutineContext, you can choose a different Job implementation — the SupervisorJob.
Let the container rescue you
It is designed to ensure that the subroutine failure will not affect other subroutines in the container. The container is also designed to handle the subroutine itself, rather than propagate exceptions.
You can create the CoroutineScope val uiScope = CoroutineScope(container job) to ensure that exceptions are not propagated.
If an exception is not processing, CoroutineContext also provides no exception handler CoroutineExceptionHandler (later), will use the default exception handler. On the JVM, exceptions are printed to the console; On Android, no matter what happens to the scheduler, your application will crash.
π₯ No matter what type of Job you use, uncaught exceptions will eventually be thrown.
The same code of conduct applies to the coroutineScope builders, the coroutineScope and supervisorScope. Each of them creates a subscope (Job or container Job as Parent) that helps you group coroutines logically (if you want to do parallel computation, or if they affect each other).
Warning: the container is only visible when it is part of one of two scopes it is designed to create: the scope that is created using the CoroutineScope(container container).
Job θΏζ― SupervisorJob οΌπ€
When to use Job? When is the container handling container handled?
When you don’t want exceptions causing the parent coroutine and sibling coroutine to be canceled, use the container container container for handling the container container.
Take a look at this example:
// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(SupervisorJob())
scope.launch {
// Child 1
}
scope.launch { // Child 2 } Copy the code
In this case, child#1 fails, and neither scope nor child#2 is cancelled.
Another example:
// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(Job())
scope.launch {
supervisorScope {
launch {
// Child 1 } launch { // Child 2 } } } Copy the code
In this case, the container container is designed to create a small scope that carries the container container job. If child#1 fails, child#2 will not be canceled. If the coroutineScope is used instead of the supervisorScope, however, the exception is propagated and unscoped.
The test! Who is my father? π―
Can you determine which Job is the parent of child#1 by looking at the following code snippet?
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
// new coroutine -> can suspend
launch {
// Child 1
} launch { // Child 2 } } Copy the code
The parent Job of child#1 is of type Job! I hope you’re right! While at first glance you might think it’s the SupervisorJob, it’s not. Because in this case, each new coroutine is always assigned a new Job, which is overwritten by the container Job. The container is created by the parent coroutine through scope. Launch. That said, in the case above, the container job isn’t doing any good.
So, whether an exception occurs in child#1 or child#2, it will propagate to scope and cause all coroutines started by it to be canceled.
Keep in mind that the container is only visible when it’s part of one of two scopes: the scope that is created using the container container container or the CoroutineScope(container container container). Passing the container job as a parameter to the coroutine builder isn’t going to produce the effect you’re expecting.
When it comes to exceptions, if a child coroutine throws an exception, it’s going to be handled by the subroutine itself and it’s not going to be propagated in the container.
The principle of
If you’re curious about how Job works, check out the implementation of childCancelled and notifyCancelling in the jobsupport. kt file.
For the container job implementation, the childCancelled() method simply returns false, indicating that it won’t propagate exceptions and won’t handle them either.
Troubleshooting π©π
In coroutines, exceptions can be handled using normal syntax: try/catch or the built-in function runCatching (which uses try/catch internally).
We said earlier that uncaught exceptions are always thrown. But different coroutine builders handle exceptions differently.
Launch
In Launch, exceptions are thrown as soon as they occur. Therefore, you can use try/catch to wrap code around exceptions. As follows:
scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) {
// Handle exception
} } Copy the code
In Launch, exceptions are thrown as soon as they occur.
Async
When async is used in the root coroutine (a direct child of the CoroutineScope instance or container job), exceptions are not raised automatically, but are not raised until you call.await().
To handle an exception thrown by async, you can call await in a try/catch.
supervisorScope {
val deferred = async {
codeThatCanThrowExceptions()
}
try {
deferred.await() } catch(e: Exception) { // Handle exception thrown in async } } Copy the code
In the above example, async calls never throw exceptions, so there is no need to wrap a try/catch. The await() method will throw an exception that occurs inside async.
Notice that in the code above we are using the supervisorScope to call async and await. As we said earlier, the container job is designed to let the coroutine handle exceptions on its own. In contrast, a Job propagates an exception, so the catch block is not called.
coroutineScope {
try {
val deferred = async {
codeThatCanThrowExceptions()
}
deferred.await() } catch(e: Exception) { // Exception thrown in async WILL NOT be caught here // but propagated up to the scope } } Copy the code
In addition, coroutines created by other coroutines will be propagated automatically if an exception occurs, regardless of your coroutine builder.
Here’s an example:
val scope = CoroutineScope(Job())
scope.launch {
async {
// If async throws, launch throws without calling .await()
}
} Copy the code
In the above example, if async encounters an exception, it is immediately thrown. Since the immediate child coroutine of scope is started by scope.launch, async inherits the Job in the coroutine context, causing it to automatically propagate exceptions to its parent.
β οΈ Exceptions thrown by coroutineScope builders or by coroutines started by other coroutines are not caught by try/catch!
In the section, SupervisorJob we mentioned CoroutineExceptionHandler. Now let’s dig into it.
CoroutineExceptionHandler
Coroutines in the exception handler is CoroutineExceptionHandler CoroutineContext an optional element, it can help you handle uncaught exception.
The following code demonstrates how to define a CoroutineExceptionHandler. Whenever an exception is caught, you get information about the CoroutineContext where the exception occurred, as well as information about the exception itself.
val handler = CoroutineExceptionHandler {
context, exception -> println("Caught $exception")
}
Copy the code
The exception will be caught if the following requirements are met:
- When β°: is thrown by a coroutine that can automatically throw an exception (
launch
Rather thanasync
) - Where π: in the context of a coroutine or root coroutine (
CoroutineScope
The direct subcoroutine of orsupervisorScope
)
Let’s look at the use of two CoroutineExceptionHandler examples.
In the following example, the exception is caught by the handler:
val scope = CoroutineScope(Job())
scope.launch(handler) {
launch {
throw Exception("Failed coroutine")
}
} Copy the code
Here is another example where the handler is used in an internal coroutine that does not catch exceptions:
val scope = CoroutineScope(Job())
scope.launch {
launch(handler) {
throw Exception("Failed coroutine")
}
} Copy the code
Because the handler is not used in the correct coroutine context, the exception is not caught. If an exception occurs in the coroutine launched by the internal launch, it will be automatically propagated to the parent coroutine. The parent coroutine is not aware of the existence of the handler, so the exception will be directly thrown.
Even if your application does not perform as expected due to exceptions, elegant exception handling is important for a good user experience.
When you’re trying to prevent the coroutine from being canceled due to automatic propagation of exceptions, it’s important to remember to use the Job when it’s not visible.
Uncaught exceptions will be propagated, catch them and provide a good user experience!
That’s it for this article, the last one left in this series.
When I mentioned coroutine cancellation earlier, I introduced coroutine scopes such as viewModelScope that cancel automatically with the lifecycle. But what should I do if I don’t want to cancel? The next article will explain.
I am bingxin said, pay attention to me, do not get lost!
This article is formatted using MDNICE