The Truth About Kotlin’s Non-Cancellable Coroutine Context
It doesn’t actually do anything — but does that matter, or could its name alone be enough to make it useful?

Naming is important, and naming is hard. As software engineers, we’re reminded of that every day. But it’s rare to find a programming problem that hinges entirely on naming. After all, choosing a name for something can’t affect what it does, right? Maybe not, but still, I’m increasingly convinced that programming is all about communication.
In Kotlin, calling withContext(NonCancellable)
creates a coroutine scope that’s protected from cancellation. Not only does this guard important code from unwanted interruptions, but it’s also useful when you want to run asynchronous cleanup code in a finally
block. Normally, a coroutine wouldn’t be able to call asynchronous functions while being cancelled, but with NonCancellable
, it’s possible.
It turns out, though, that withContext(NonCancellable)
isn’t as clever as it sounds. In fact, its behaviour is exactly equivalent to withContext(Job())
. And while Job
is a simple and familiar building block that’s used throughout the coroutines API, NonCancellable
is a point solution that’s unfamiliar and infrequently used. So why bother with it at all?
Well, it’s all about the name. “Job” conveys next to nothing about the functionality we want, whereas “non-cancellable” is a clear indicator of intent. Or is it? I think “non-cancellable” is ambiguous — and the behaviour of withContext(NonCancellable)
doesn’t exactly match the straightforward interpretation. One thing I find particularly unintuitive, based on the name, is that it does not actually stop your coroutines from being cancelled.
“Non-cancellable” is ambiguous — and it does not actually stop your coroutines from being cancelled.
If the name is misleading, why would I recommend using it? Isn’t it doing more harm than good? Well, withContext(Job())
isn’t necessarily much better, especially for a newcomer. What’s a Job
? And what does it have to do with cancellation?
I’m glad you asked. Actually, creating a coroutine in Kotlin always creates an associated Job
object. That’s obvious with background coroutine builders like launch
and async
, which actually return the newly-created job, but it’s equally true for foreground coroutine scopes created with functions like coroutineScope
and withContext
.
Jobs form a hierarchy, where each job can have child jobs associated with it. A coroutine is typically launched from inside an existing coroutine, and the new job becomes a child of the coroutine job that created it.
When creating a new coroutine, it’s possible to replace the parent by passing a custom job as a parameter. The NonCancellable
coroutine context element is an example of a custom job that can be used in this way.
It’s important to notice that the innerJob
in this example is a different job from the NonCancellable
job. Calling withContext
created a new job, because creating a coroutine always creates a job. Normally the new job would have determined its parent job by looking at the existing coroutine context, but we passed a custom job instead. So the NonCancellable
job is just standing in for the new job’s parent.
The result of this substitution is that the new coroutine scope becomes oblivious to things that are happening outside of it. Cancelling a parent job would normally cancel its children. But the outerJob
in this example is no longer a parent of the innerJob
, and has no control over it. If the outer job is cancelled, the inner job won’t even know that the cancellation took place.
The NonCancellable
job, meanwhile, doesn’t do anything at all when you try to cancel it. Actually, since it’s an immutable singleton, it doesn’t keep track of its child jobs either. With the NonCancellable
job as its new parent, the inner job is basically a free agent. How’s that for hands-off parenting?
In fact, NonCancellable
doesn’t affect the behaviour of its child jobs in any way. That means that jobs which use NonCancellable
as their parent are not, themselves, immune from cancellation.
Despite how it sounds, using withContext(NonCancellable)
does not create a coroutine scope that can’t be cancelled. The newly-created job can be cancelled just like any other coroutine. And all the coroutines launched inside this scope will be children of that new (cancellable) job, so a failure in one child job will still cause the others to be cancelled. This scope’s only special behaviour is that it doesn’t inherit cancellation, and won’t get automatically cancelled when the coroutine that created it is cancelled.
As it turns out, there’s a second way to get that same behaviour. What we get with NonCancellable
is a custom job that can replace the parent job for a new coroutine, breaking the link between the new scope and the outer coroutine that created it. Well, creating a new parentless job using the Job()
constructor function achieves exactly the same effect.
But wait: that new job we created and passed to withContext
is a normal mutable job that can be cancelled! Isn’t that a problem? Well, no. Remember, the custom job that we provide is only being used as the parent for the new job that withContext
will always create.
We do care if our custom parent job is cancelled, because that would cancel all the child coroutines running in the withContext
scope. But we’re passing it directly to the withContext
function, so there’s nobody with a reference to it who could manually call its cancel
function. And it doesn’t have a parent, so it can’t inherit a cancellation that way. That means the closest it can get to being cancelled is to fail, which happens if its child job — the withContext
scope — fails. And if the withContext
job fails, it’s going to cancel all of its child coroutines anyway.
Detaching a coroutine from the normal job hierarchy isn’t something that should be done very often. The coroutine job hierarchy isn’t just there for fun — it provides three important behaviours.
- A job always waits for its children to complete, so that we can’t leak resources by losing track of coroutines we’ve launched.
- A job fails if one of its children fails, so that errors always propagate upwards and don’t get forgotten.
- A job is cancelled if its parent fails, so that we don’t wait for unneeded coroutines to finish, or leak resources by abandoning them before they complete.
The reason withContext(NonCancellable)
and withContext(Job())
are acceptable is because the withContext
function actually alters two of those structured concurrency behaviours. Like the similar coroutineScope
function, withContext
is designed to help you break down a single suspend
function into multiple concurrent sub-tasks. As part of its implementation, it already suspends and waits for all of its child coroutines to complete. It also provides foreground error handling for the child coroutines, because the suspended call to withContext
itself will abort with an exception if any of the coroutines launched within its scope fails.
That means that when we sever the parent–child relationship between the withContext
block and its containing coroutine, we still retain two out of the three structured concurrency behaviours. Even without a link to a parent's job, the withContext
block still waits for the completion or failure of its children before continuing. The only thing we lose is the ability for the child coroutines to be cancelled by cancellation or failure in the outside world.
Passing a custom parent job to a background coroutine is another story, though. If you do that, the application won’t be able to wait for the new job to complete, and won’t detect or handle any errors that happen within the job. Don’t pass a custom job like NonCancellable
or Job()
to a background coroutine builder like launch
or async
.
The NonCancellable
job reminds me of those convenient simplifications that are often used in high school science classes. You know the ones — like the way electrons are supposed to spin in circles round atoms — where they tell you on day one of your university degree that it wasn’t really true at all. For a newcomer reading an unfamiliar codebase or copying code from a half-understood StackOverflow answer, withContext(NonCancellable)
is a much more descriptive piece of code than withContext(Job())
. You can immediately grasp that this is something to do with preventing coroutine cancellation, and if you’re lucky, you never have to think about it any further than that.
But even after you’ve climbed Wittgenstein’s ladder, and realised that NonCancellable
doesn’t really need to exist, I think you should keep using it. Because now you’re the person writing the StackOverflow answer, or the senior developer maintaining the code that newcomers need to be able to understand. When those junior programmers go online to search for “Kotlin Job,” they’ll be lucky to find much more than recruitment listings. But when they search for “Kotlin NonCancellable,” well… with any luck, they might find your blog post about it.