When working with Kotlin coroutines, one of the most common issues developers face is the “Job was cancelled” error.
This error can occur unexpectedly during coroutine execution and often confuses beginners who are just learning about coroutine lifecycle and structured concurrency.
In this article, we will explore why this error happens, what it means, and how to handle it effectively in real-world applications.
Understanding the “Job was cancelled” Error
A coroutine in Kotlin is tied to a Job
, which represents its lifecycle.
When a coroutine is launched, the job can be in various states: Active, Completed, or Cancelled.
If a coroutine gets cancelled before completing its work, Kotlin throws a CancellationException
with the message “Job was cancelled”.

This behavior is by design, as coroutine cancellation is a cooperative mechanism.
It means that when the parent coroutine or scope is cancelled, all child coroutines are also cancelled.
For example, if you launch coroutines in an Activity
scope in Android, once the activity is destroyed, the coroutine jobs are automatically cancelled.
Common Scenarios That Trigger the Error
- Parent scope cancelled: If the parent coroutine scope is cancelled, all child coroutines throw “Job was cancelled”.
- Timeout exceeded: When using
withTimeout
orwithTimeoutOrNull
, coroutines will be cancelled after the time limit. - Manual cancellation: If you explicitly call
job.cancel()
on a coroutine job. - Activity/Fragment lifecycle in Android: When the lifecycle owner is destroyed, associated coroutine scopes cancel their jobs.
Example of the Error
Let’s see a simple case where the error appears:
import kotlinx.coroutines.* fun main() = runBlocking { val job = launch { repeat(10) { i -> println("Processing $i ...") delay(500) } } delay(1200) println("Cancelling job...") job.cancel() job.join() println("Job cancelled") }
In this example, the coroutine starts repeating a task, but after 1200ms we manually cancel it.
The coroutine will throw a CancellationException
internally, and you will see the message “Job was cancelled” in logs.
How to Handle the Error
In most cases, you don’t need to catch the CancellationException
because it is a normal part of coroutine cancellation.
However, if you want to perform cleanup or release resources, you should handle it properly.
import kotlinx.coroutines.* fun main() = runBlocking { val job = launch { try { repeat(10) { i -> println("Downloading chunk $i ...") delay(500) } } catch (e: CancellationException) { println("Job cancelled due to: ${e.message}") } finally { println("Releasing resources...") } } delay(1200) println("Cancelling download...") job.cancel(CancellationException("Network issue")) job.join() println("Coroutine finished") }
Here we use a try-catch-finally
block.
The coroutine will still be cancelled, but before termination, it executes the finally
block, allowing you to free up resources such as closing a file, releasing a database connection, or stopping an ongoing network request.
Best Practices to Avoid Issues
- Always consider lifecycle: In Android, use
lifecycleScope
orviewModelScope
to automatically cancel jobs when the lifecycle ends. - Use structured concurrency: Launch coroutines within a proper scope instead of using
GlobalScope
. - Handle timeouts carefully: When using
withTimeout
, provide fallback mechanisms. - Cleanup resources: Always release resources inside
finally
when a coroutine is cancelled.
Comparison: Normal Exception vs CancellationException
Aspect | Normal Exception | CancellationException |
---|---|---|
Cause | Unexpected error (e.g., NullPointerException) | Intentional cancellation of coroutine job |
Default Handling | Crashes coroutine unless caught | Considered normal and propagated silently |
Need to Handle? | Yes, must be caught | No, unless cleanup is required |
Real-World Example in Android
Suppose you are making a network request inside an Android ViewModel
.
If the user navigates away, the coroutine should be cancelled. Without proper handling, you may encounter “Job was cancelled” in logs.
class MyViewModel : ViewModel() { fun fetchData() { viewModelScope.launch { try { val data = simulateNetworkRequest() println("Received: $data") } catch (e: CancellationException) { println("Coroutine cancelled: ${e.message}") } } } private suspend fun simulateNetworkRequest(): String { delay(5000) // Simulating long network call return "Server Response" } }
In this case, if the user leaves the screen before 5 seconds, the coroutine will be cancelled automatically by viewModelScope
.
This prevents memory leaks and ensures efficient resource management.
Conclusion
The “Job was cancelled” error in Kotlin coroutines is not actually an error but a signal that a coroutine was cancelled as expected.
By understanding how coroutine jobs work, why they are cancelled, and how to properly handle cancellation with try-catch-finally
, you can build stable and robust applications.
Always use structured concurrency, respect lifecycle scopes, and ensure cleanup in finally
blocks for better coroutine management.