Coroutine Exceptions & Cancellation
Structured concurrency makes errors and cancellation predictable — but only if you know the rules. Exceptions propagate differently in launch and async , and cancellation is cooperative.
Learn Coroutine Exceptions & Cancellation in our free Kotlin course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a…
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
By the end you'll handle failures with CoroutineExceptionHandler and supervisorScope , and write cancellation-aware code.
What You'll Learn in This Lesson
1️⃣ Exception Propagation: launch vs async
launch propagates exceptions eagerly to its parent; a CoroutineExceptionHandler is the last resort. async defers its exception until you call await() , so you wrap that in try/catch.
A handler catches uncaught launch failures; async failures appear at await() .
2️⃣ Isolating Failures with supervisorScope
With a regular Job , one failing child cancels its siblings. A SupervisorJob or supervisorScope isolates failures so the rest keep running.
The failing child is handled locally, and its sibling completes unaffected.
3️⃣ Cooperative Cancellation
Cancellation throws a CancellationException at the next suspension point. Cooperate by checking isActive , never swallow that exception, and use withContext(NonCancellable) for essential cleanup.
Your turn. Replace the TODO s, then run and compare.
Run two async loads in a supervisorScope so one failing doesn't stop the other.
📋 Quick Reference — Errors & Cancellation
Practice quiz
When does an exception thrown inside a 'launch' coroutine propagate?
- Immediately, propagating up to the parent/scope (and can crash if unhandled)
- It is silently ignored
- Only when you call join()
- It is returned as a value
Answer: Immediately, propagating up to the parent/scope (and can crash if unhandled). launch propagates exceptions eagerly to its parent; if nothing handles them they reach the CoroutineExceptionHandler or crash.
When is an exception from an 'async' coroutine actually thrown to you?
- Immediately when async starts
- Never
- When you call await() on its Deferred
- Only in release builds
Answer: When you call await() on its Deferred. async defers the exception; it is re-thrown at the call to await(), so you wrap await in try/catch.
What is a CoroutineExceptionHandler used for?
- Catching exceptions from async/await
- A last-resort handler for uncaught exceptions in launch coroutines
- Cancelling all coroutines
- Logging successful results
Answer: A last-resort handler for uncaught exceptions in launch coroutines. It installs a top-level handler for uncaught exceptions in launch-style builders; it does not catch async/await exceptions.
How does a normal Job differ from a SupervisorJob when a child fails?
- SupervisorJob ignores all exceptions
- A SupervisorJob is faster
- They are identical
- With a normal Job a failing child cancels its siblings; with a SupervisorJob siblings keep running
Answer: With a normal Job a failing child cancels its siblings; with a SupervisorJob siblings keep running. A SupervisorJob isolates failures: one child failing does not cancel the others, unlike a regular Job.
Which builder runs a block where a child's failure does NOT cancel its siblings?
- supervisorScope { }
- coroutineScope { }
- runBlocking { }
- withTimeout { }
Answer: supervisorScope { }. supervisorScope provides supervisor semantics: children fail independently without taking down their siblings.
How should you handle an expected exception inside a single coroutine?
- Only with a SupervisorJob
- A try/catch around the suspending call
- There is no way to catch it
- By restarting the app
Answer: A try/catch around the suspending call. A plain try/catch inside the coroutine works for handling exceptions from suspend calls locally.
Which check lets a long loop cooperate with cancellation?
- Looping forever
- isCancelled() returning false
- Thread.interrupted()
- isActive (or ensureActive() / yield())
Answer: isActive (or ensureActive() / yield()). Cancellation is cooperative; checking isActive, calling ensureActive(), or yield() lets the coroutine stop when cancelled.
Why should you NOT swallow a CancellationException in a catch block?
- It is never thrown
- It is a checked exception
- Because cancellation relies on it propagating; swallowing it breaks structured concurrency
- It improves performance to keep it
Answer: Because cancellation relies on it propagating; swallowing it breaks structured concurrency. CancellationException is the signal that drives cooperative cancellation; catching and ignoring it prevents the coroutine from actually cancelling.
When cancelled, what do suspending functions do at the next suspension point?
- Return null
- Throw a CancellationException so the coroutine can unwind
- Loop forever
- Start a new thread
Answer: Throw a CancellationException so the coroutine can unwind. Cancellation surfaces as a CancellationException thrown at the next suspension point, letting the coroutine unwind cleanly.
What is withContext(NonCancellable) used for?
- To run essential cleanup code that must complete even during cancellation
- To make a coroutine run faster
- To disable all exceptions
- To start a new thread
Answer: To run essential cleanup code that must complete even during cancellation. Wrapping cleanup in withContext(NonCancellable) lets suspend calls in a finally block finish even though the coroutine is being cancelled.