By Lukas Lechner

7 Common mistakes you might be making when using Kotlin Coroutines

Translator: Bingxin said

In my opinion, Kotlin Coroutines greatly simplify both synchronous and asynchronous code. However, I have found that many developers make generic mistakes when using coroutines.

1. Instantiate a new Job instance when using coroutines

Sometimes you need a job to perform some operation on a coroutine, for example, cancel later. Also, since the coroutine builders launch{} and async{} both require a job as an argument, you might want to create a new job instance to use as an argument. That way, you have a job reference whose.cancel() method you can call later.

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

There seems to be nothing wrong with this code, and the coroutine has been successfully cancelled.

>_ 

performing some work in Coroutine
Coroutine was cancelled

Process finished with exit code 0
Copy the code

However, let’s try running the coroutine in CoroutineScope, and then take the job that nullifys the CoroutineScope instead of the coroutine.

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

When a scope is cancelled, all coroutines within it are cancelled. But when we execute the modified code again, this is not the case.

>_

performing some work in Coroutine

Process finished with exit code 0
Copy the code

Now, the Coroutine was not cancelled, Coroutine was cancelled and not printed.

Why is that?

It turns out that in order to make asynchronous/synchronous code more secure, coroutines offer a revolutionary feature called “structured concurrency.” One mechanism for structured concurrency is that when a scope is cancelled, all coroutines in that scope are cancelled. To ensure that this mechanism works, the hierarchical structure before scoped jobs and coroutine jobs looks like this:

In our example, something unusual happened. By passing our own job instance to the coroutine builder launch(), we don’t actually bind the new job instance to the coroutine itself. Instead, it becomes the parent job of the new coroutine. So the parent job of the new coroutine you create is not a coroutine-scoped job, but a newly created job object.

Therefore, the coroutine job and the coroutine scoped job are not related at this point.

We broke structured concurrency so that when we scoped the coroutine, it was no longer cancelled.

The solution is to use the job returned by launch() directly.

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

In this way, the coroutine can be cancelled with the cancellation of the scope.

>_

performing some work in Coroutine
Coroutine was cancelled

Process finished with exit code 0
Copy the code

SupervisorJob is used incorrectly

Sometimes you’ll use supervisorjobs to do the following:

  1. Stop abnormal propagation in the job inheritance system
  2. Failure of one coroutine does not affect other coroutines

Because the coroutine builders launch{} and async{} can both pass the Job as an input, you can consider passing the SupervisorJob instance to the builder.

launch(SupervisorJob()){
    // Coroutine Body
}
Copy the code

However, just like error 1, this breaks the cancellation mechanism for structured concurrency. The correct solution is to use the supervisorScope{} function.

supervisorScope {
    launch {
        // Coroutine Body}}Copy the code

3. Cancellation is not supported

When you perform some heavy operations in your own defined Suspend function, such as calculating the Fibonacci sequence:

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

There is a problem with this suspend function: it does not support cooperative cancellation. This means that even if the coroutine that executes this function is cancelled early, it will continue to run until the computation is complete. To avoid this, periodically execute the following functions:

  • ensureActive()
  • isActive()
  • yield()

The following code uses ensureActive() to support cancellation.

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

Suspend functions in the Kotlin standard library (such as delay()) can be cancelled in conjunction. But for your own suspended functions, don’t forget to consider cancellations.

4. Switch the scheduler when making a network request or database query

This one isn’t really a “bug,” but it can still make your code difficult to understand and even less efficient. Some developers believe that when a coroutine is called, you should switch to a background scheduler, such as Retrofit suspend for network requests or Room suspend for database operations.

This is not necessary. Since all suspend functions should be main-thread safe, Retrofit and Room follow this convention. You can read my article to learn more.

5. Try to handle exceptions to a coroutine with a try/catch

The exception handling of coroutines is complex, and it took me quite a while to fully understand it and explain it to other developers through my blog and lectures. I have also made some diagrams to sum up this complex topic.

One of the least intuitive aspects of Kotlin coroutine exception handling is that you can’t use a try-catch to catch an exception.

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

If you run the above code, the exception will not be handled and the application will crash.

>_ 

Exception in thread "main" java.lang.Exception

Process finished with exit code 1
Copy the code

Kotlin Coroutines allow us to write asynchronous code in the traditional way. However, when it comes to exception handling, the traditional try-catch mechanism is not used as most developers would like. If you want to handle exceptions, within coroutines use try-catch directly or use CoroutineExceptionHandler.

Read the aforementioned article for more information.

6. Use CoroutineExceptionHandler in child coroutines

Come again a concise: used in son coroutines builder CoroutineExceptionHandler won’t have any effect. This is because exception handling is proxyed to the parent coroutine. Because, you must be in the root or the parent coroutines or the use of CoroutineExceptionHandler CoroutineScope.

Again, read here for more details.

7. Capture CancellationExceptions

When a coroutine is cancelled, the pending function that is executing throws a CancellationException. This usually causes the coroutine to “exception” and stop running immediately. As shown in the following code:

fun main(a) = runBlocking {

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

    delay(500)
    job.cancel()
}
Copy the code

After 500 ms, the suspended function delay() throws a CancellationException, the coroutine “terminates abnormally” and stops running.

>_

Performing network request in Coroutine

Process finished with exit code 0
Copy the code

Now let’s assume that delay() represents a network request, and to handle any exceptions that might occur on a network request, we use a try-catch block to catch all exceptions.

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

Now, suppose there is a bug on the server. The catch branch catches not only httpExceptions for incorrect network requests, but also CancellationExceptions. So the coroutine does not “stop abnormally” but continues to run.

>_

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

Process finished with exit code 0
Copy the code

This can result in a waste of device resources and even crash in some cases.

To solve this problem, we can just catch HttpException.

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

Or raise CancellationExceptions again.

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

So those are the seven most common mistakes you can make with Kotlin Coroutines. If you know of any other common errors, feel free to leave them in the comments!

Also, don’t forget to share this article with other developers in case such mistakes happen. Thanks!

Thank you for reading, and have a great day!