Introduction
When working with Kotlin coroutines, one of the most common sources of confusion is the difference between cancelling a specific Job
and cancelling all coroutines within a CoroutineScope
.
They sound similar — but their behavior is quite different.
In this article, we’ll break it down clearly using real-world analogies and practical code examples — so you’ll know exactly which one to use and when.
The basics: Job
vs CoroutineScope
A Job
represents a single coroutine. It’s returned by launch
or async
, and gives you control over that specific coroutine — you can cancel it, check its status, or wait for it to finish.
A CoroutineScope
, on the other hand, is a container for coroutines. It holds a CoroutineContext
, which usually includes a Job
that manages the lifecycle of all coroutines launched within the scope.
Think of it like this:
A
CoroutineScope is a manager, and each
Job is a worker.
The manager can hire workers (launch new coroutines), and shutting down the manager (i.e. cancelling the scope’sJob) sends everyone home.
Key differences in practice
Example: Job.cancel()
val scope = CoroutineScope(Dispatchers.Default) val jobA = scope.launch { delay(5000) println("Job A done") } val jobB = scope.launch { delay(10000) println("Job B done") } jobA.cancel() // Only cancels Job A
In this case:
jobA
will be cancelledjobB
continues unaffected- The scope remains active and can still launch new coroutines
Example: cancelling the whole scope
val scope = CoroutineScope(Dispatchers.Default) val jobA = scope.launch { delay(5000) println("Job A done") } val jobB = scope.launch { delay(10000) println("Job B done") } (scope.coroutineContext[Job] ?: error("No Job in context")).cancel() // Cancels ALL coroutines
Or, if you’re using MainScope()
, viewModelScope
, or a custom scope where cancel()
is exposed:
val scope = MainScope() scope.cancel() // Convenient API: cancels the internal Job
Here:
- Both
jobA
andjobB
are cancelled - The underlying
Job
enters the cancelled state - The scope becomes inactive — no new coroutines can be launched
Quick note
The CoroutineScope
interface itself does not define a cancel() function.
What you’re really doing is cancelling the Job that the scope holds in its context. Many scopes — like
MainScope()
, viewModelScope
, or your own custom scopes — include a Job
, and either expose cancel()
directly or handle cancellation automatically.
After cancelling a scope
Once a CoroutineScope
is cancelled (i.e., its internal Job
is cancelled):
- Any new
launch {}
orasync {}
calls will not run - The scope is considered inactive
- This behavior is especially important in Android components like
ViewModel
orService
, where coroutine scopes are tied to the component’s lifecycle
Real-world analogy
Imagine you’re running a small workshop:
- Each
Job
is a worker doing their assigned task - The
CoroutineScope
is the workshop manager who oversees them
Now:
- If you cancel a specific
Job
, you’re saying:
→ “Hey Anna, stop what you’re doing.” - If you cancel the manager’s contract — i.e. the
Job
that backs theCoroutineScope
— you’re saying:
→ “We’re shutting down the entire workshop — everyone go home, and don’t come back.”
In practice, you’re cancelling the manager’s Job, not the scope itself.
But since the scope is backed by that Job, the effect is the same:
once cancelled, no more workers can be hired, and all existing tasks are stopped.
Job Offers
When to use each
Here’s a practical guide:
- Want to cancel a single coroutine?
→ Usejob.cancel()
- Want to cancel all coroutines and deactivate the scope?
→ Cancel theJob
that backs the scope — either directly viajob.cancel()
, or (in scopes likeMainScope
) viascope.cancel()
- Need cleanup at the end of a lifecycle (e.g.,
Activity
,ViewModel
)?
→ Use lifecycle-aware scopes provided by Android, or cancel the backingJob
manually if you’re using a custom scope
A note on Android scopes: viewModelScope
and lifecycleScope
In Android development, many components provide lifecycle-aware coroutine scopes out of the box:
viewModelScope
is cancelled automatically when theViewModel
is clearedlifecycleScope
is cancelled when theActivity
orFragment
is destroyed
This means you typically don’t need to cancel these scopes manually — the Android framework takes care of that for you.
However, understanding how coroutine cancellation works under the hood is important — especially when working outside the component lifecycle, such as in a Service
, Repository
, or any custom background task where you manage your own CoroutineScope
.
Summary
job.cancel()
affects only one specific coroutinescope.cancel()
(if available), or cancelling the scope’s underlyingJob
, stops all coroutines within that scope- Use
scope.cancel()
orjob.cancel()
when working with a custom scope that requires explicit cleanup (e.g., in aService
or long-running background task) - Android scopes like
viewModelScope
andlifecycleScope
are cancelled automatically — there’s no need to callcancel()
manually - Use
job.cancel()
when you only need to stop an individual coroutine
Understanding the difference helps you write more predictable, maintainable, and robust coroutine-based systems — especially in real-world, production-grade applications.
Further Reading
If you’re preparing for Kotlin interviews or want to test your coroutine knowledge, check out this companion article:
Kotlin Coroutines Interview Questions You’ll Actually Get Asked
This is a friend link — free to read even without a Medium membership.
If you found this article useful, consider sharing it with others who might benefit.
I write about practical Kotlin and Android topics, focusing on real-world use cases and clean architecture — feel free to follow for future posts.
This article was previously published on proandroiddev.com.