How to break a coroutine cancellation

Vasiliy Nikitin
MobilePeople
Published in
3 min readSep 24, 2022

Let’s take a look at the next example:

We launch one coroutine and after some time cancelling it. Inside a coroutine we call some other suspend function in the loop. In this example networkCall() do nothing but delay, but in production code, it may be some I/O operation. As we know, all I/O operations can throw an exception, so wrapping it in a try..catch construct is a common way to handle possible errors. In idiomatic Kotlin, runCatching is often used instead of the try..catch statement and our example will work the same in both cases. I think you have seen such code in your project, it looks correct. Let’s run it:

It is expected that networkCall() will not be called after canceling the coroutine. But something went wrong: it is called and even the message “Coroutine is finished” is printed. In this case, a delay() call after the cancellation does not actually delay, but all not-suspending functions are executing normally. Looks like our coroutine is broken and in some “semi-cancelled” state. So, what’s happening?

(Not so) Deep dive into coroutine cancellation

When you call cancel() on Job (or CoroutineScope), the current job changes its state to “Cancelling” and notifies all currently suspended continuations about cancellation. At this time, all suspended continuations are resumed with a special CancellationException. All future suspend calls in this coroutine will also be immediately resumed with this exception (unless you use the NonCancellable context). The cancellation also propagates to all child (nested) coroutines.

So, the only way a coroutine can be interrupted at any point (not literally any, but only on suspend calls) of its execution is by throwing an exception. In the Kotlin Coroutines framework, CancellationException is used for this. This exception propagates up the call stack, but does not crash the application because it is handled separately by the framework.

CancellationException has some subclasses and you can also inherit it yourself. This can be useful for specifying different cancellation reasons and specifying different coroutine behavior depending on the exception.

Solution

Thus, to ensure that the coroutine can be correctly canceled you should never swallow the CancellationException. Otherwise, coroutine will continue execution until a new suspend call. Unfortunately, runCatching operator roughly catches all Throwables.

Flow’s catch() operator respect the CancellationException and handles it appropriately.

The better way to catch exceptions in coroutines is the following “fixed” runCatching statement:

It handles cancellation correctly, and our example now works as expected:

To summarize, you should never silently catch a CancellationException inside a coroutine, and always rethrow it if it is caught.

--

--