Cancelling Kotlin Coroutines

Jitendra Alekar
3 min readDec 6, 2021

Overview

This article covers cancelling coroutine jobs, parent-child relationships, and limiting lifetime using scopes.

Understanding Cancellation

Coroutines provide fine-grained control over concurrent execution. We can cancel a coroutine or limit the execution of several coroutines and avoid unnecessary work. When we cancel a coroutine, a CancellationException is thrown and is treated as a normal completion state.

Example Use Case for Cancellation

Let’s say we have a use-case where the user can perform a search operation. As part of this search operation, we make an API call. Meanwhile, the user dismisses the flow by navigating to the previous screen. Now, we don’t require our API result and should cancel the network request. We will see how we can achieve this by the end of this tutorial.

Cancelling a Job

Coroutines launched using the launch coroutine builder return a reference of a Job. We use launch for fire and forget use-cases as they do not return any value. We can use job.cancel() method to cancel the execution. Let’s look at an example:

fun main() = runBlocking {
val job = launch {
println("Start job 1")
delay(2000)
println("End job 1")
}
delay(1000L)
job.cancel()
job.join()
assert(job.isCompleted)
}
assert(job.isCompleted) //this is true

This produces the following output:

Start job 1

In the previous example, we used the join() method to wait for the job to complete and then checked that the job has completed. We can safely call join() on a cancelling or cancelled job.

Cancelling a Deferred — A Job With a Result

We also have another scope builder which returns an instance of Deferred. A Deferred is a subtype of Job, which also provides a result. Since it is a sub-type of Job we can call cancel() on it. Let’s look at an example for cancelling a Deferred:

fun main() = runBlocking {
val deferredJob = async {
println("Start job 1")
delay(2000)
println("End job 1")
1
}
delay(1000L)
deferredJob.cancel()
//deferredJob.await()// Will throw a jobCancellationException
}

This produces the following output:

Start job 1

In the above example, we use Async builder, which returns a reference of a Deferred. We simply call cancel on it to cancel the coroutine. We will not get a value using await() if the coroutine is cancelled. Calling await() on cancelled coroutines throws a JobCancellationException.

Parent-Child Relationship

Cancelling a parent coroutine also cancels its children.

In the following example, we launch two coroutines in the parent job. Then we use the cancelAndJoin() method, which cancels the job as well as waits for its completion. As a result, the child jobs are also cancelled. Let’s see the code:

fun main() = runBlocking {
val parentJob = launch {
launch {
println("Start child 1")
delay(2000)
println("End child 1")
}
launch{
println("Start child 2")
delay(2000)
println("End child 2")
}
}
delay(1000)
parentJob.cancelAndJoin()
}

This produces the following output:

Start child 1
Start child 2

Using Scopes to Limit Execution

We can define custom scopes to limit the lifetime of coroutines. The following example defines a scope object with a new Job and I/O dispatcher:

fun main() = runBlocking {
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch{
launch {
println("Start child 1")
delay(2000)
println("End child 1")
}
launch {
println("Start child 2")
delay(2000)
println("End child 2")
}
}
delay(1000)
scope.cancel()
}

This produces the following output:

Start child 1
Start child 2

In the previous example, on calling cancel() on the scope, all coroutines launched as part of this scope are cancelled.

Conclusion

This article illustrated how to cancel coroutines. The sample code is available on Github.

--

--