Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 3.5

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

9. Manage Cancellation
Written by Nishant Srivastava

When you initiate multiple asynchronous operations that are dependent on each other, the possibilities of one failing, then leading to others also failing, increases. This means that the result is not going to end as you expected. Coroutines address this problem and provide mechanisms to handle this and many other cases.

This chapter will dive deeper into the concepts and mechanics of cancellation in coroutines.

Cancelling a coroutine

As with any multi-threading concept, the lifecycle of a coroutine can become a problem. You need to stop any potentially long-running background tasks when it is in an inconsistent state in order to prevent memory leaks or crashes. To resolve this, coroutines provide a simple cancelling mechanism.

Job object

As you’ve seen in Chapter 3: “Getting Started with Coroutines,” when you launch a new routine using the launch coroutine builder, you get a Job object as the return value. This Job object represents the running coroutine, which you can cancel at any point by calling the cancel function.

This is interesting because with Kotlin coroutines, you have the ability to specify a parent job as a context for multiple coroutines, and calling cancel on the parent coroutine will result in all coroutines being canceled.

Note: When you cancel the parent coroutine, all of its children are recursively cancelled, too.

The launch coroutine builder is used as a fire-and-forget way of starting a coroutine. It is similar to starting a new thread. If the code inside the coroutine that was started from launch terminates with an exception, the system treats it like an uncaught exception in a thread — usually printed to stderr in backend JVM applications — and the Android applications crash. You use join to wait for the completion of the launched coroutine but it does not propagate its exception. However, a crashed child coroutine cancels its parent with the corresponding exception, too.

Cancel

In a long-running application, you might need fine-grained control on your background coroutines. For example, a task that launched a coroutine might have finished, and now its result is no longer needed; consequently, its operation can be canceled. This is where the cancel method comes in.

In order to cancel a coroutine, you simply need to call the cancel method on the Job object that was returned from the coroutine builder. Calling the cancel function on a Job, or on a Deferred instance, will stop the inner computation on a coroutine if the handling of the isActive flag is properly implemented.

Coroutine cancelation is cooperative. This means that the suspending function has to cooperate in order to support cancelling. In practice, the suspending function has to periodically test the isActive property, which is set to false when the coroutine is canceled. This applies to your suspending functions, too. All suspending functions provided by the Kotlin coroutine library support cancelation already.

Note: isActive is checked between child coroutine suspension points by the standard library, so you only have to check isActive in your own long-running computations.

In the code snippet below, the launch function returns a Job that can be used to cancel the running coroutine:

fun main() = runBlocking {
  val job = launch {
    repeat(1000) { i ->
      println("$i. Crunching numbers [Beep.Boop.Beep]...")
      delay(500L)
    }
  }
  delay(1300L) // delay a bit
  println("main: I am tired of waiting!")
  job.cancel() // cancels the job
  job.join() // waits for job’s completion
  println("main: Now I can quit.")
}

Note: You can find the executable version of the above snippet of code in the starter project in the file called CancelCoroutine.kt.

Crunching numbers via coroutine
Crunching numbers via coroutine

Output:

0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
main: I am tired of waiting!
main: Now I can quit.

CancellationException

Coroutines internally use CancellationException instances for cancellation, which are then ignored by all handlers. They are typically thrown by cancellable suspending functions if the Job of the coroutine is canceled while it is suspending. It indicates normal cancellation of a coroutine.

Note: CancellationException is not printed to the console/log by the default uncaught exception handler.

When you cancel a coroutine using the cancel function on its Job object without a cause, it terminates but it does not cancel its parent. Cancelling without cause is a mechanism for a parent to cancel its children without canceling itself.

The following piece of code shows an example of CancellationException handling when child jobs are canceled, which is pretty straightforward:

fun main() = runBlocking {
  val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught original $exception")
  }
  val parentJob = GlobalScope.launch(handler) {
    val childJob = launch {
      // Sub-child job
      launch {
        // Sub-child job
        launch {
          throw IOException()
        }
      }
    }

    try {
      childJob.join()
    } catch (e: CancellationException) {
      println("Rethrowing CancellationException" +
          " with original cause")
      throw e
    }
  }
  parentJob.join()
}

Note: You can find the executable version of the above snippet of code in the starter project in the file called CancellationExceptionExample.kt.

Output:

Rethrowing CancellationException with original cause
Caught original java.io.IOException

Join, CancelAndJoin and CancelChildren

The Kotlin standard library provides a couple of convenience functions for handling coroutine completion and cancellation.

  1. When using coroutines, you will most likely be interested in the result of a completed job. To know about the completion of the coroutine, the join function is available, which suspends the coroutine execution until the canceled job is complete:

    fun main() = runBlocking {
      val job = launch {
        println("Crunching numbers [Beep.Boop.Beep]...")
        delay(500L)
      }
    
      // waits for job’s completion
      job.join()
      println("main: Now I can quit.")
    }
    

    Note: You can find the executable version of the above snippet of code in the starter project in the file called JoinCoroutineExample.kt.

    Output:

    Crunching numbers [Beep.Boop.Beep]...
    main: Now I can quit.
    
  2. If you would like to wait for the completion of more than one coroutine, then you should use the joinAll function:

    fun main() = runBlocking {
      val jobOne = launch {
        println("Job 1: Crunching numbers [Beep.Boop.Beep]...")
        delay(500L)
      }
    
      val jobTwo = launch {
        println("Job 2: Crunching numbers [Beep.Boop.Beep]...")
        delay(500L)
      }
    
      // waits for both the jobs to complete
      joinAll(jobOne, jobTwo)
      println("main: Now I can quit.")
    }
    

    Note: You can find the executable version of the above snippet of code in the starter project in the file called JoinAllCoroutineExample.kt.

    Output:

    Job 1: Crunching numbers [Beep.Boop.Beep]...
    Job 2: Crunching numbers [Beep.Boop.Beep]...
    main: Now I can quit.
    
  3. If you would like to cancel and then wait for the completion of a coroutine, then a cancelAndJoin function that combines the two is also provided:

    fun main() = runBlocking {
      val job = launch {
        repeat(1000) { i ->
          println("$i. Crunching numbers [Beep.Boop.Beep]...")
          delay(500L)
        }
      }
      delay(1300L) // delay a bit
      println("main: I am tired of waiting!")
      // cancels the job and waits for job’s completion
      job.cancelAndJoin()
      println("main: Now I can quit.")
    }
    

    Note: You can find the executable version of the above snippet of code in the starter project in the file called CancelAndJoinCoroutineExample.kt

    Output:

    0. Crunching numbers [Beep.Boop.Beep]...
    1. Crunching numbers [Beep.Boop.Beep]...
    2. Crunching numbers [Beep.Boop.Beep]...
    main: I am tired of waiting!
    main: Now I can quit.
    
  4. If your coroutine has multiple child coroutines and you would like to cancel all of them, then you should use the cancelChildren method:

    fun main() = runBlocking {
      val parentJob = launch {
        val childOne = launch {
          repeat(1000) { i ->
            println("Child Coroutine 1: " +
                "$i. Crunching numbers [Beep.Boop.Beep]...")
            delay(500L)
          }
        }
    
        // Handle the exception thrown from `launch`
        // coroutine builder
        childOne.invokeOnCompletion { exception ->
          println("Child One: ${exception?.message}")
        }
    
        val childTwo = launch {
          repeat(1000) { i ->
            println("Child Coroutine 2: " +
                "$i. Crunching numbers [Beep.Boop.Beep]...")
            delay(500L)
          }
        }
    
        // Handle the exception thrown from `launch`
        // coroutine builder
        childTwo.invokeOnCompletion { exception ->
          println("Child Two: ${exception?.message}")
        }
    
      }
      delay(1200L)
    
      println("Calling cancelChildren() on the parentJob")
      parentJob.cancelChildren()
    
      println("parentJob isActive: ${parentJob.isActive}")
    }
    

    Note: You can find the executable version of the above snippet of code in the starter project in the file called CancelChildren.kt.

    Output:

    Child Coroutine 1: 0. Crunching numbers [Beep.Boop.Beep]...
    Child Coroutine 2: 0. Crunching numbers [Beep.Boop.Beep]...
    Child Coroutine 1: 1. Crunching numbers [Beep.Boop.Beep]...
    Child Coroutine 2: 1. Crunching numbers [Beep.Boop.Beep]...
    Child Coroutine 1: 2. Crunching numbers [Beep.Boop.Beep]...
    Child Coroutine 2: 2. Crunching numbers [Beep.Boop.Beep]...
    Calling cancelChildren() on the parentJob
    parentJob isActive: true
    Child One: Job was canceled
    Child Two: Job was canceled
    

This is all nice, but how do you cancel a coroutine after a set time? The next section covers that specific scenario.

Timeout

Long-running coroutines are sometimes required to terminate after a set time has passed. While you can manually track the reference to the corresponding Job and launch a separate coroutine to cancel the tracked one after a delay, the coroutines library provides a convenience function called withTimeout.

Take a look at the following example:

fun main() = runBlocking {
  withTimeout(1500L) {
    repeat(1000) { i ->
      println("$i. Crunching numbers [Beep.Boop.Beep]...")
      delay(500L)
    }
  }
}

Note: You can find the executable version of the above snippet of code in the starter project in the file called WithTimeoutExample.kt.

Output:

0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1500 MILLISECONDS
...

The TimeoutCancellationException that withTimeout throws is a subclass of CancellationException. You haven’t seen its stack trace printed on the console before. That is because, inside a canceled coroutine, CancellationException is considered to be a normal reason for coroutine completion. However, in this example, you have used withTimeout right inside the main function.

Because cancellation is just an exception, you close all the resources in the usual way. You can wrap the code with a timeout in a try {…} catch (e: TimeoutCancellationException) {…} block if you need to do some additional action, specifically on any kind of timeout or use the withTimeoutOrNull function:

fun main() = runBlocking {
  try {
    withTimeout(1500L) {
      repeat(1000) { i ->
        println("$i. Crunching numbers [Beep.Boop.Beep]...")
        delay(500L)
      }
    }
  } catch (e: TimeoutCancellationException) {
    println("Caught ${e.javaClass.simpleName}")
  }
}

Note: You can find the executable version of the above snippet of code in the starter project in the file called TimeoutCancellationExceptionHandling.kt.

Output:

0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Caught TimeoutCancellationException

If you want to set a timeout for a coroutine Job, wrap the suspended code with the withTimeoutOrNull function, which will return null in case of timeout:

fun main() = runBlocking {
  val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
      println("$i. Crunching numbers [Beep.Boop.Beep]...")
      delay(500L)
    }
    "Done" // will get canceled before it produces this result
  }
  // Result will be `null`
  println("Result is $result")
}

Note: You can find the executable version of the above snippet of code in the starter project in the file called WithTimeoutOrNullExample.kt.

Output:

0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Result is null

Key points

  • When the parent coroutine is canceled, all of its children are recursively canceled, too.
  • CancellationException is not printed to the console/log by the default uncaught exception handler.
  • Using the withTimeout function, you can terminate a long-running coroutine after a set time has elapsed.

Where to go from here?

Being able to cancel an ongoing task is almost always required. The cycle of starting a coroutine and canceling it when an exception is thrown or when the business logic demands it is part of some of the common patterns in programming. Coroutines in Kotlin were built keeping that in mind since the very beginning.

Next up, you will explore how to efficiently process collections involving more than one processing step using coroutines.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.