7 common mistakes you might be making when using Kotlin Coroutines

In my opinion, Kotlin Coroutines really simplify the way we write asynchronous and concurrent code. However, I identified some common mistakes that many developers make when using Coroutines.

Common Mistake #1: Instantiating a new job instance when launching a Coroutine

Sometimes you need a job as a handle to your Coroutine in order to, for instance, cancel it later. And since the two Coroutine builders launch{} and async{} both take a job as input parameter, you might think about creating a new job instance and then pass this new instance to such a builder as launch{}. This way, you have a reference to the job and therefore are able to call methods like .cancel() on it.

fun main() = runBlocking {

    val coroutineJob = Job()
    launch(coroutineJob) {
        println("performing some work in Coroutine")
        delay(100)
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine was cancelled")
        }
    }

    // cancel job while Coroutine performs work
    delay(50)
    coroutineJob.cancel()
}

This code seems to be fine. When we run it, the Coroutine gets cancelled successfully:

>_ 

performing some work in Coroutine
Coroutine was cancelled

Process finished with exit code 0

However, let’s now run this Coroutine in a CoroutineScope and cancel this scope instead of the Coroutine’s job:

fun main() = runBlocking {

    val scopeJob = Job()
    val scope = CoroutineScope(scopeJob)

    val coroutineJob = Job()
    scope.launch(coroutineJob) {
        println("performing some work in Coroutine")
        delay(100)
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine was cancelled")
        }
    }

    // cancel scope while Coroutine performs work
    delay(50)
    scope.cancel()
}

All Coroutines in a scope should be cancelled when the scope itself gets cancelled. However, when we run this updated code example, this is not the case:

>_

performing some work in Coroutine

Process finished with exit code 0

Now, the Coroutine is not cancelled, as “Coroutine was cancelled” is never printed out.

Why is that?

Well, in order to make your asynchronous and concurrent code safer, Coroutines have this innovative feature called “Structured Concurrency”. One mechanism of “Structured Concurrency” is to cancel all Coroutines of a CoroutineScope if the scope gets cancelled. In order for this mechanism to work, a hierarchy between the Scope’s job and the Coroutine’s job is formed, as the image below illustrates:

In our case however, something very unexpected happens. By passing your own Job instance to the launch() Coroutine builder, we don’t actually define this new instance as the Job that is associated with the Coroutine itself! Instead, it becomes the parent job of the new coroutine. So the parent of our new Coroutine is not the Coroutine Scope‘s job, but our newly instantiated job object.

Therefore, the job of the Coroutine isn’t connected to the job of the CoroutineScope anymore:

So we broke Structured Concurrency and therefore the Coroutine is not cancelled anymore when the we cancel our scope.

The solution for this problem is to simply use the job that launch{} returns as a handle for our Coroutine:

fun main() = runBlocking {
    val scopeJob = Job()
    val scope = CoroutineScope(scopeJob)

    val coroutineJob = scope.launch {
        println("performing some work in Coroutine")
        delay(100)
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine was cancelled")
        }
    }

    // cancel while coroutine performs work
    delay(50)
    scope.cancel()
}

This way, our Coroutine now is cancelled when the Scope is cancelled:

>_

performing some work in Coroutine
Coroutine was cancelled

Process finished with exit code 0

Common Mistake #2: Installing SupervisorJob the wrong way

Sometimes you want to install a SupervisorJob somewhere in your job hierarchy in order to

  1. stop exceptions from propagating up the job hierarchy
  2. not cancel the siblings of Coroutines if one of them fails

Since the Coroutine builders launch{} and async{} can take a Job as an input parameter, you could think about achieving this by passing a SupervisorJob to these builders:

launch(SupervisorJob()){
    // Coroutine Body
}

However, like in Mistake#1, you are breaking the cancellation mechanism of Structured Concurrency. The solution for this issue is to use the supervisorScope{} scoping function instead:

supervisorScope {
    launch {
        // Coroutine Body
    }
}

Common Mistake #3: Not supporting cancellation

Let’s say you want to perform an expensive operation, like the calculation of a factorial number, in your own suspend function:

// factorial of n (n!) = 1 * 2 * 3 * 4 * ... * n
suspend fun calculateFactorialOf(number: Int): BigInteger =
    withContext(Dispatchers.Default) {
        var factorial = BigInteger.ONE
        for (i in 1..number) {
            factorial = factorial.multiply(BigInteger.valueOf(i.toLong()))
        }
        factorial
    }

This suspend function has a problem: It doesn’t support “cooperative cancellation”. This means that even if the coroutine in which it is executed is cancelled prematurely, it will still continue to run until the calculation is completed. To avoid this issue, we periodically have to either use

Below, you can find a solution that supports cancellation by using ensureActive():

// factorial of n (n!) = 1 * 2 * 3 * 4 * ... * n
suspend fun calculateFactorialOf(number: Int): BigInteger =
    withContext(Dispatchers.Default) {
        var factorial = BigInteger.ONE
        for (i in 1..number) {
            ensureActive()
            factorial = factorial.multiply(BigInteger.valueOf(i.toLong()))
        }
        factorial
    }

The suspend functions in the Kotlin standard library (like e.g. `delay()`) are all cooperative regarding cancellation, but for your own suspend functions, you should never forget to think about cancellation.


📬 Get notified whenever I publish new content! 🚀

No Spam! Unsubscribe at any time!


Common Mistake #4: Switching the dispatcher when performing a network request or database query

Well, this one is not that really a “mistake”, but still makes your code a bit harder to read and maybe also a little bit less efficient: Some developers think that they need to switch to a background dispatcher in their Coroutines before calling, for instance, a suspend function of Retrofit in order to make a network request, or a suspend function of Room to perform a database operation.

This is not required because of the convention that all suspend function should be “main-safe”, and fortunately Retrofit and Room stick to this convention. You can read more about this topic in another article on my blog:


Common Mistake #5: Trying to handle an exception of a failed coroutine with try/catch

Exception Handling in Coroutines is hard. I have spent quite some time to really understand it and also explained it to other developers in the form of a Blogpost and several Conference Talks. I even created a Cheat Sheet that summarises this complex topic.

One of the most unintuitive aspects of exception handling with Kotlin Coroutines is that you aren’t able to catch exceptions with try/catch of Coroutines that fail with an exception:

fun main() = runBlocking<Unit> {
    try {
        launch {
            throw Exception()
        }
    } catch (exception: Exception) {
        println("Handled $exception")
    }
}

If you run the code above, the exception will not be handled and the app will crash:

>_ 

Exception in thread "main" java.lang.Exception

Process finished with exit code 1

Kotlin Coroutines promise us to be able use conventional coding constructs in asynchronous code. However, in this case this is not true, as the conventional try-catch block does not handle the exception, as most developers would expect. If you want to handle the exception, you have to either use try-catch directly in your Coroutine or install a CoroutineExceptionHandler.

For more information, please have a look at the aforementioned article


Common Mistake #6: Installing a CoroutineExceptionHandler in a Child Coroutine

Let’s keep this one short and sweet: Installing a CoroutineExceptionHandler via the coroutine builder of a child coroutine won’t have any effect. This is because exception handling is delegated to the Parent Coroutine. Therefore, you have to install your CoroutineExceptionHandlers either in your root or parent Coroutines or in the CoroutineScope.

Again, details can be found here


Common Mistake #7: Catching CancellationExceptions

When a Coroutine gets canceled, the suspend function that is currently executed in the Coroutine will throw a CancellationException. This usually completes the Coroutine "exceptionally" and therefore the execution of the Coroutine will stop immediately, like in the following example:

fun main() = runBlocking {

    val job = launch {
        println("Performing network request in Coroutine")
        delay(1000)
        println("Coroutine still running ... ")
    }

    delay(500)
    job.cancel()
}

After 500 milliseconds, the suspend function delay() will throw a CancellationException, the Coroutine "completes exceptionally" and therefore stops its execution:

>_

Performing network request in Coroutine

Process finished with exit code 0

Now let’s image that delay() represents a network request and in order to handle exceptions of that network request, we surround it with a try-catch block that catches all Exceptions:

fun main() = runBlocking {

    val job = launch {
        try {
            println("Performing network request in Coroutine")
            delay(1000)
        } catch (e: Exception) {
            println("Handled exception in Coroutine")
        }

        println("Coroutine still running ... ")
    }

    delay(500)
    job.cancel()
}

Now, we introduced a severe bug. The catch clause will not only catch HttpExceptions for failed network requests, but also CancellationExceptions! Therefore the Coroutine will not "complete exceptionally" but instead will continue to run:

>_

Performing network request in Coroutine
Handled exception in Coroutine
Coroutine still running ... 

Process finished with exit code 0

This will lead to wasted device resources and in some situations this can even lead to crashes.

To fix this problem, we could either catch HttpExceptions only:

fun main() = runBlocking {

    val job = launch {
        try {
            println("Performing network request in Coroutine")
            delay(1000)
        } catch (e: HttpException) {
            println("Handled exception in Coroutine")
        }

        println("Coroutine still running ... ")
    }

    delay(500)
    job.cancel()
}

or re-throw CancellationExceptions:

fun main() = runBlocking {

    val job = launch {
        try {
            println("Performing network request in Coroutine")
            delay(1000)
        } catch (e: Exception) {
            if (e is CancellationException) {
                throw e
            }
            println("Handled exception in Coroutine")
        }

        println("Coroutine still running ... ")
    }

    delay(500)
    job.cancel()
}

So these are 7 of the most common mistakes when using Kotlin Coroutines. If you know other common mistakes, then please let me know in the comments!

Also, please don’t forget to share this article so that other developers won’t make these mistakes. Thanks!

Thank you for reading, and have a great day! 🌴

Btw: If you want to deepen your knowledge about Kotlin Coroutines and Flow, I can recommend my 15+hour long online course:

This image has an empty alt attribute; its file name is course-image-with-text-1024x576.png

📬 Get notified whenever I publish new content! 🚀

No Spam! Unsubscribe at any time!


 

Lukas Lechner

I'm a Freelance Android Developer and Online Course Creator. I have many years of professional experience as an Android Developer and worked on some of the biggest mobile apps projects in Germany and Austria. I like to write articles for my blog and speak at meetups and conferences.