Sitemap
Coding Kinetics

Master Kotlin without the overwhelm. Cut through the noise. Learn your way — alone or in good company.

Introduction to Structured Concurrency: Coroutine Exception & Cancellation Mechanisms

Part 2: How Cancellations and Exceptions Affect Job Hiearchies

7 min readAug 18, 2025

--

Foreword: This article is made possible thanks to my Patreon supporters, who specifically requested a deeper dive into the topic of coroutine cancellations and exceptions. This is a follow-up to my Droidcon NYC 2025 talk on Kotlin Coroutine Mechanisms — if you missed it, you can view the slides here.

Topic Navigation

If you haven’t already read Part 1, be sure to take the time to do so. The previous article sets you up for understanding the nuances of scope and context needed for studying exceptions andcancellations.

Press enter or click to view image in full size

In the world of Kotlin coroutines, cancellations are like kind of kitchen fires: a fire starts and can’t be contained until there are safety the “blast radius” of a cancellation can propagate upwards to the parent scope, when unhandled... But did you know that you can control the “blast radius” with structured concurrency?

What is Structured Concurrency?

Kotlin coroutines are designed for structured concurrency, but this can only be achieved with the intentional use of Kotlin coroutines. The Structured Concurrency paradigm can be boiled down to two general rules:

  1. A parent coroutine always waits for its children to complete.
  2. A child coroutine never gets lost.

When you follow these rules, you have structured concurrency.

Today, we focus on the second rule in particular: A child coroutine never gets lost.

In structured concurrency, a child coroutine is always tracked by its parent. Unhandled failures or cancellations are propagated up to the parent.

Coroutine Cancellations

Cancellations in coroutines can come about in two ways: either called programmatically (by you), or they can happen as a result of an exception.

Setup

We’ll test out these cancellation concepts using the last segment we worked with in the previous article, Kitchen.kt: no kitchen fires, and no explosions. Not yet, anyway.

If you run the code as-is, you’ll see the following output in it’s entirety:

Making salad                | current thread: DefaultDispatcher-worker-1 @coroutine#2
Making pasta | current thread: DefaultDispatcher-worker-2 @coroutine#3
Cooking pasta... | current thread: DefaultDispatcher-worker-2 @coroutine#5
Boiling water... | current thread: DefaultDispatcher-worker-2 @coroutine#4
Salad is ready! | current thread: DefaultDispatcher-worker-1 @coroutine#2
Pasta is cooked. | current thread: DefaultDispatcher-worker-1 @coroutine#5
Water is hot! | current thread: DefaultDispatcher-worker-1 @coroutine#4
Pasta is ready! | current thread: main @coroutine#1
All dishes are prepared!

The salad and pasta orders run simultaneously, TPDP

Press enter or click to view image in full size

Alright! Let’s start messing with this code via cancellations. We’ll be using just the cancel() API to demonstrate what happens when cancel() is placed in different areas of Kitchen.kt.

Canceling a Job

Canceling a job will cancel the job and the children itself, but the scope will not be canceled. In Kitchen.kt, add a cancel() statement before delay:

Running this code reveals that “Salad is ready!” never prints, but we can still launch other jobs on the scope.

Making salad                | current thread: DefaultDispatcher-worker-1
Making pasta | current thread: DefaultDispatcher-worker-2
Boiling water... | current thread: DefaultDispatcher-worker-3
Cooking pasta... | current thread: DefaultDispatcher-worker-4
Pasta is cooked. | current thread: DefaultDispatcher-worker-1
Water is hot! | current thread: DefaultDispatcher-worker-1
Pasta is ready! | current thread: main
All dishes are prepared! | current thread: main

saladOrder is canceled, yet the kitchenScope remains active. That is why we see pastaOrder able to start and finish the work. pastaOrder is unaffected, since it exists as a sibling coroutine.

Press enter or click to view image in full size

Because cancel() is actually an extension function of CoroutineScope, calling cancel cancels the current coroutine, but not the kitchenScope or the parentJob.

There are some conclusions that can be drawn from coroutine cancellation:

  • By default, canceling a child job propagates cancellation downwards to all child jobs.
  • Canceling a child job does not cancel the sibling or the parent.

While coroutine cancellations can be clean enough, the same is not true for coroutine exceptions. Coroutine cancellations is like cancelling the order. We don’t need it anymore, but the other orders can go through.

Coroutine Exceptions

If a coroutine cancellation is like order cancellation, then coroutine exception is more like a kitchen fire (blast radius and all). In other words, something unexpected happened.

In Kitchen.kt, replace the cancel() call in saladOrder with throw Exception(“Ack! Kitchen fire”):

If a cancel() affects the current coroutine, we know that boilWater will be affected by an exception. But what about the parent job and parent scope?

Let’s run the problem to find out:

Making pasta                | current thread: DefaultDispatcher-worker-2
Making salad | current thread: DefaultDispatcher-worker-1
Exception in thread "DefaultDispatcher-worker-1" java.lang.RuntimeException: Exception while trying to handle coroutine exception
at kotlinx.coroutines.CoroutineExceptionHandlerKt.handlerException(CoroutineExceptionHandler.kt:36)
...
... 15 more
Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception: Ack! Kitchen fire
at org.example.KitchenKt$main$1$saladOrder$1.invokeSuspend(Kitchen.kt:11)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:829)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@55506de, Dispatchers.Default]
Exception in thread "main" kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelled}@23e028a9
Caused by: java.lang.Exception: Ack! Kitchen fire
at org.example.KitchenKt$main$1$saladOrder$1.invokeSuspend(Kitchen.kt:11)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:829)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelled}@55506de, Dispatchers.Default]

Process finished with exit code 1

And there it is: a “kitchen fire” starts at the salad order. Because the exception is unhandled in launch, there are no safety measures in place to handle the exception. As a result, a thrown exception in launch propagates up to the parent coroutine.

Press enter or click to view image in full size

When the exception bubbles up to the parent coroutine, the exception immediately cancels the parent job. As a result, the remainder of existing child coroutines are also canceled.

When an exception is unhandled, it will propagate upwards, causing siblings to cancel as well.

Handle an Exception with try/catch

One way to handle coroutine exceptions is to wrap them in a try/catch statement. In Kitchen.kt, wrap the inside of the saladOrder with a try/catch statement like so:

Now, the try/catch statement can catch and handle the exception that occurs in saladOrder. Although saladOrder is not able to complete, we can see the exception handled in logging, and pastaOrder is able to continue to execute.

Making pasta                | current thread: DefaultDispatcher-worker-2
Making salad | current thread: DefaultDispatcher-worker-1
Unable to create salad order. Cause: Ack! Kitchen fire | current thread: DefaultDispatcher-worker-1
Boiling water... | current thread: DefaultDispatcher-worker-1
Cooking pasta... | current thread: DefaultDispatcher-worker-3
Pasta is cooked. | current thread: DefaultDispatcher-worker-1
Water is hot! | current thread: DefaultDispatcher-worker-1
Pasta is ready! | current thread: main
All dishes are prepared! | current thread: main

The problem with try/catch is that a try/catch must be created for every coroutine. This quickly makes it hard to maintain code in a scalable manner.

Using SupervisorJob to isolate the failure

There is another way to catch exceptions at a more generic root level: by replacing the parent job with a SupervisorJob type.

In Kitchen.kt, replace the parent Job with a SupervisorJob type and remove the try/catch statement from earlier to better understand what SupervisorJob really does on its own:

Running the program once more, even though a kitchen fire starts with the saladOrder, the failure is contained using SupervisorJob.

Making salad                | current thread: DefaultDispatcher-worker-1
Making pasta | current thread: DefaultDispatcher-worker-2
Boiling water... | current thread: DefaultDispatcher-worker-3
Cooking pasta... | current thread: DefaultDispatcher-worker-4
Exception in thread "DefaultDispatcher-worker-1" java.lang.RuntimeException: Exception while trying to handle coroutine exception
at kotlinx.coroutines.CoroutineExceptionHandlerKt.handlerException(CoroutineExceptionHandler.kt:36)
at kotlinx.coroutines.internal.CoroutineExceptionHandlerImpl_commonKt.handleUncaughtCoroutineException(CoroutineExceptionHandlerImpl.common.kt:38)
at kotlinx.coroutines.CoroutineExceptionHandlerKt.handleCoroutineException(CoroutineExceptionHandler.kt:31)
at ...
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
Suppressed: java.lang.Exception: Ack! Kitchen fire
at org.example.KitchenKt$main$1$saladOrder$1.invokeSuspend(Kitchen.kt:11)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
... 5 more
Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception: Ack! Kitchen fire
at org.example.KitchenKt$main$1$saladOrder$1.invokeSuspend(Kitchen.kt:11)
at kotlin.coroutines.jvm.internal...
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@3399b913, Dispatchers.Default]
Pasta is cooked. | current thread: DefaultDispatcher-worker-1
Water is hot! | current thread: DefaultDispatcher-worker-1
Pasta is ready! | current thread: main
All dishes are prepared! | current thread: main

Process finished with exit code 0

Normally, if. a child coroutine fails, the exception is propated up to the parent. But now, that parent job is a SupervisorJob, which acts as a container for the kitchen fire. In effect the Supervisor job does not cancel the parent or a coroutine’s siblings.

Press enter or click to view image in full size

With the saladOrder blast radius is contained, the rest of the kitchen can keep running. A you can see from the output, using a SupervisorJob does not mean that the exception is automatically suppressed or auto-handled. The scope of this article does not cover a lot of exception handling, but if you’re looking for more, your next topic should be on CoroutineExceptionHandler.

That’s all there is for today. Thanks for reading this work, and thanks to the patrons who voted for this topic.

Topic Navigation

Looking for more on Structured Concurrency?

Coroutines can feel tricky, and structured concurrency feels elusive at times. I’ll keep sharing insights like this each week, so if you want to keep learning alongside me, join us over on Patreon. I’ve linked additional resources I’ve written on this topic for a convenient nd impactful learning experience. Until next time!

--

--

Coding Kinetics
Coding Kinetics

Published in Coding Kinetics

Master Kotlin without the overwhelm. Cut through the noise. Learn your way — alone or in good company.

Amanda Hinchman
Amanda Hinchman

Written by Amanda Hinchman

Kotlin GDE, Android engineer & O'Reilly book author | Support my research on Patreon: patreon.com/AmandaHinchman

Responses (1)