Home Android & Kotlin Books Kotlin Coroutines by Tutorials

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.

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.

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.")
}
Crunching numbers via coroutine
Qliyjsipv qorpegw kie zaceekaxi

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.

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()
}
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.

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.

fun main() = runBlocking {
  withTimeout(1500L) {
    repeat(1000) { i ->
      println("$i. Crunching numbers [Beep.Boop.Beep]...")
      delay(500L)
    }
  }
}
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
...
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}")
  }
}
0. Crunching numbers [Beep.Boop.Beep]...
1. Crunching numbers [Beep.Boop.Beep]...
2. Crunching numbers [Beep.Boop.Beep]...
Caught TimeoutCancellationException
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")
}
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.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.