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.
Mini-Challenge: Robust Parallel Loads
Run two async loads in a supervisorScope so one failing doesn't stop the other.
Common Errors (and the fix)
- Swallowing CancellationException — Rethrow it; otherwise the coroutine never actually cancels.
- Expecting a handler to catch async errors — Those surface at await() ; wrap it in try/catch.
- Tight loops that ignore cancellation — Check isActive or call ensureActive() / yield() .
- Suspending in cleanup after cancellation — Wrap it in withContext(NonCancellable) .
Pro Tips
- 💡 Catch specific exceptions rather than a broad Exception so CancellationException still propagates.
- 💡 Use supervisorScope when independent tasks shouldn't take each other down.
- 💡 Built-in suspend functions like delay() already check cancellation for you.
📋 Quick Reference — Errors & Cancellation
Concept
Kotlin Syntax
Top-level handler
CoroutineExceptionHandler { _, e -> }
Catch async error
try { d.await() } catch (e) { }
Isolate failures
supervisorScope { ... }
Cooperate
while (isActive) { ... }
Cancel a job
job.cancelAndJoin()
Cleanup safely
withContext(NonCancellable) { }
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ launch propagates eagerly; async defers to await()
- ✅ CoroutineExceptionHandler is a last resort for launch failures
- ✅ SupervisorJob / supervisorScope isolate child failures
- ✅ Cancellation is cooperative — check isActive
- ✅ Never swallow CancellationException ; clean up with NonCancellable
- ✅ Next lesson: KSP & Annotation Processing
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.
Continue this course
- Previous: Gradle & the Kotlin Build System
- Next: KSP & Annotation Processing