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.