Coroutines (Part II) – Job, SupervisorJob, Launch and Async

This is the second article of a 3 part series on Coroutines. If you haven’t read the first article, I highly suggest you do

After having learned the fundamentals we can finally start using Coroutines in code. We’ll start by learning about Job and SupervisorJob and then proceed to creating Coroutines with launch and async.

Photo by Thom Milkovic on Unsplash

Job

For every coroutine that is created, a Job instance is returned to uniquely identify that coroutine and allow you to manage its lifecycle. You can also create Jobs manually if necessary, for example you might want to give it to a CoroutineContext to manage the context’s lifecycle.

Conceptually, a job is a cancellable thing with a life-cycle that culminates in its completion. […] A coroutine that threw CancellationException is considered to be cancelled normally. If a cancellation cause is a different exception type, then the job is considered to have failed.

Kotlin Docs

A Job has a define set of states: New, Active, Completing, Completed, Cancelling and Cancelled.

We can’t access the states themselves, but we can access properties of a Job: isActive, isCancelled and isCompleted.

If you pass in a particular Job or a SupervisorJob as a context, such as in launch(SupervisorJob()), it doesn’t cause the work to run in that Job, it just means it’ll use that Job as a parent Job for a Job it’ll construct. Why? Because AbstractCoroutine implements Job and all coroutines extend AbstractCoroutine.

Parent-Child Hierarchy

  • When a parent’s job is canceled, the children’s jobs are canceled;
  • If we cancel the child’s job, the parent’s job continues;
  • When the parent’s job throws an exception, the children’s jobs are canceled too;
  • When a child’s job throws an exception, the parent may be canceled;
  • Parent cannot complete until all its children are complete;

Supervisor Job

It is similar to a regular Job with the only exception that its children can fail independently of each other.

A child’s failure or cancellation does not result in the supervisor’s job failing or affecting its other children, therefore a supervisor can create a unique policy for dealing with its children’s failures.

A SupervisorJob only works as described when it’s part of a scope: either created using supervisorScope or CoroutineScope(SupervisorJob()). Passing a SupervisorJob as a parameter of a coroutine builder will not have the desired effect you would’ve thought for cancellation.

Here’s their behavior side by side using launch:

As you can see, by using a SupervisorJob, the failure of one child doesn’t affect the other.

Here’s their behavior using async:

That’s probably not what you were expecting, the reason that happens is that launch propagates the exception up while await throws the exception. We’ll take a deeper look that in part III.

Coroutine builders

Coroutine builders are simple functions that generate a new coroutine to execute a given suspending function. They differ from typical non-suspending functions in that they do not suspend themselves and so serve as a link between the normal and suspending worlds.

In the context of runBlocking, the given suspending function and its children in the call hierarchy will effectively block the current thread until it finishes executing. This is often used from the main() function to give a sort of top-level coroutine from which to work, keep the JVM alive while doing so and on tests because you want your tests execution to block until all the work has completed.

There are two main ways to start coroutines, and they have different uses:

Launch

The launch builder will start a new coroutine but won’t wait for its completion, that means it won’t return the result to the caller. launch acts as a link between ordinary functions and coroutines.

The coroutine context is inherited from the CoroutineScope but you can specify more elements by passing another context as the context parameter. Remember that the CoroutineContext is never overridden, but rather joined with the existing one.

By default, the coroutine is immediately scheduled for execution. You can change that by specifying the start parameter. You can read about the allowed values here.

If you take a look at the return type, you can see it’s a Job. That means you can control the coroutine lifecycle by interacting with that Job. You can easily cancel it by calling job.cancel().

As we saw earlier, launch is used a lot on ViewModels to create a bridge from non-suspending code to suspending code.

One thing I see people doing wrong in the beginning is adding a try catch around the launch block. That catch will never run even if an exception is thrown.

You need to remember that when you launch a new coroutine from a non suspending function, you’re basically changing worlds in your code. The code that started the coroutine and the coroutine now run separately.

To fix that you simply need to move your try catch block inside the launch block or add a CoroutineExceptionHandler to your scope’s context.

Async

The async builder will start a new coroutine and it allows you to get the returned value by calling await.

The same thing I said about the context and start parameters apply here.

await waits for the block to finish without blocking the thread and resumes when the deferred operation is finished, returning the result or raising the corresponding exception. You might think that wrapping the await with a try catch would work, it kinda works but not as you expect. What do you think the code below outputs?

Output:

Caught exception 
Exception in thread "DefaultDispatcher-worker-1" 

That looks very confusing the first time you look at it, here’s what happening:

  1. The coroutine launched by async throws an exception
  2. The exception is caught and “Caught exception” is printed
  3. The coroutine delegates the exception handling to its parent
  4. The parent is cancelled because it has no exception handler

How do you correctly handle exceptions then? You need to wrap the await call with supervisorScope to prevent the exception from propagating up.

You’ll understand why you need to do that later when we take a deeper look at exception handling. Now let’s try another thing, what happens if we use a SupervisorJob on the root scope?

Output:

Caught exception
Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception 
Caught exception
Exception in thread "DefaultDispatcher-worker-2" java.lang.Exception

It’s very similar to a normal job with one exception

  1. The coroutine launched by async throws an exception
  2. The exception is caught and “Caught exception” is printed
  3. The coroutine delegates the exception handling to its parent
  4. The parent is NOT cancelled because it’s a SupervisorJob, only the child is

They’re different but not that much

Both launch and nested async builders do not re-throw exceptions that occur inside them. Instead, they propagate them up the coroutine hierarchy. As we saw earlier child coroutines always delegate the exception handling to its parent.

coroutineScope

CoroutineScope with a capital C and coroutineScope with a tiny c are two different things. Here we’ll talk about the one that’s a function call.

coroutineScope creates a boundary that establishes a new CoroutineScope. The new scope inherits its coroutineContext from the outer scope, but overrides the context’s Job. It also causes the current coroutine to suspend until all child coroutines have finished their execution.

It’s very useful when we need to start new coroutines in a structured way inside a suspend function without access to the outer scope. What’s really cool is coroutineScope will create a child scope. So if the parent scope gets cancelled, it will pass the cancellation down to all the new coroutines.

For example the code below doesn’t work because async and launch are extensions on CoroutineScope

How can you fix that? You can use coroutineScope to create a scope inside a suspend function and still maintain structured concurrency.

This function is designed for parallel decomposition of work. When any child coroutine in this scope fails, this scope fails and all the rest of the children are cancelled. It’s useful when you have many async blocks that must be canceled if any of them fails.

Kotlin Docs

Remember that an exception will also cancel the parent’s scope if you don’t use a SupervisorJob.

supervisorScope

The behavior of supervisorScope is defined as: a failure of a child does not cause the scope to fail and does not affect its other children.

Well, what does that mean?

What does this code output? By looking at the definition you might think it outputs nothing because a failure of a child shouldn’t cause its scope to fail.

Here’s the output:

Exception in thread "main" java.lang.Exception
Exception in thread "main" java.lang.Exception

That’s pretty interesting, the exception is propagated up and the scope gets cancelled. Even though it sounds like you can crash child coroutines as much as you want, that’s not entirely true. Exceptions still get propagated upwards, and if parent supervisorScope doesn’t have some error handling mechanism present, it’s still gonna get cancelled.

So, how do we solve it? We can simply add a CoroutineExceptionHandler to the CoroutineContext.

Another neat feature about supervisorScope is that Coroutines created inside it become top-level Coroutines, allowing us to install a CoroutineExceptionHandler in them.

This code has the expected output, nothing is outputted.


In this article we learned about Job and SupervisorJob and most importantly how they work. We also saw that launch and async can be used to create coroutines and learned how they differ from each other. Now that we know the basic about Coroutines we can proceed to more advanced concepts, stay tuned for part III.

If you have any doubts, feel free to contact me. See you in the next article 😉

Resources

Are You Handling Exceptions in Kotlin Coroutines Properly?

Kotlin Coroutine Job Lifecycle


Cover photo by Dominik Vanyi on Unsplash