skip to content

CoroutineExceptionHandler

A CoroutineExceptionHandler installed on a root scope catches what launch would otherwise report as uncaught. The trap interviewers set is that it does nothing for async, whose failure is stored in the Deferred instead.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What is CoroutineExceptionHandler in Kotlin coroutines, and what kind of failures does it handle?

level: juniorimportance: must knowfreq 70%

answer

  1. Last-resort catcher for uncaught launch exceptions
  2. It's a CoroutineContext element with its own Key
  3. launch yes, async no (Deferred re-throws at await)
  4. Handles, never recovers/resumes
  5. Ignores CancellationException

basics

~10 s

It is a piece of coroutine context that acts as a last-resort catcher. When a coroutine started with launch fails and nobody else handles the exception, this handler runs instead of crashing silently.

solid answer

~40 s

CoroutineExceptionHandler is a CoroutineContext element (a key/value entry in the context) used as a last-resort handler for uncaught exceptions. You add it to a scope or to launch via the context parameter: launch(handler) { ... }. When a launch coroutine throws and the exception is not caught by user code and reaches a root coroutine, the runtime invokes handler.handleException(context, throwable) instead of crashing the thread (or, on the JVM, calling the default uncaught handler). It only catches; it cannot resume the coroutine. It is invoked for launch (a 'fire and forget' builder whose result nobody awaits). It is NOT consulted for exceptions from async, because those are stored in the Deferred and re-thrown at await().

code

kotlin · 5 lines
kotlin
val handler = CoroutineExceptionHandler { _, e ->
    println("caught: ${e.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch { throw IllegalStateException("oops") } // -> caught: oops

go deeper

for a junior

Knows it is a context element that catches uncaught launch exceptions and that it cannot recover the coroutine.

for a middle

Explains 'uncaught' precisely (reaches a root coroutine), and that async exceptions go through await instead.

for a senior

Discusses installation point (scope/root vs child), interplay with Job/SupervisorJob, and CancellationException being ignored.

for a principal

Frames it as a last-resort policy hook (logging/alerting) versus structured handling, and reasons about where it belongs in an app's coroutine architecture.

## What it is `CoroutineExceptionHandler` is an element of the **coroutine context** — the same map-like structure that also holds the `Job`, the `CoroutineDispatcher`, and the `CoroutineName`. Each element has a `Key`; `CoroutineExceptionHandler` is both the interface and its own key. You create one with the factory function: ```kotlin val handler = CoroutineExceptionHandler { context, throwable -> println("Caught $throwable") } ``` ## How you install it You pass it as part of the context to a scope or a builder: ```kotlin val scope = CoroutineScope(SupervisorJob() + handler) scope.launch { throw RuntimeException("boom") } // handler runs ``` ## What 'uncaught' means An exception is **uncaught** when it propagates out of your coroutine body without a surrounding `try/catch` and travels up to a **root coroutine** (a coroutine directly in a scope, not a child of another coroutine). At that point the machinery has nowhere left to deliver it, so it calls `handler.handleException(context, throwable)`. Without a handler, the JVM falls back to the thread's default uncaught-exception handler (often printing a stack trace). ## It only works for launch `launch` produces a `Job` and its result is never awaited, so a thrown exception is genuinely 'uncaught' — the handler is the only place to observe it. `async` produces a `Deferred<T>`; the exception is **encapsulated** in that Deferred and rethrown when someone calls `await()`. Because the caller is expected to handle it there, the `CoroutineExceptionHandler` is **not** invoked for the async result. ## Key limits - It is a **handler, not a recovery point** — it cannot resume or retry the coroutine. - It must be installed at the **scope or root coroutine**; putting it on an inner `launch` that is a child of another coroutine has no effect (the child delegates handling to its parent). - `CancellationException` is ignored — cancellation is normal control flow, not an error. Key APIs/keywords: `CoroutineExceptionHandler`, `handleException`, `CoroutineContext`, `Job`, `launch`, `async`, `Deferred.await`, `SupervisorJob`, `CancellationException`.

  • Why does the handler not fire for an exception thrown inside async?
    Because async stores the exception in the returned Deferred and rethrows it at await(); the caller is expected to handle it there, so it is not treated as uncaught.

It is like a smoke detector at the front door: it only alerts once the failure has escaped every room (no try/catch caught it), and it can warn you but not put out the fire.

saying these in an interview costs you the question

  • Claiming it works the same for launch and async
  • Saying it can resume or retry the failed coroutine
  • Confusing it with a regular try/catch inside the body
  • Thinking it catches CancellationException

context

open as a page

Why does a CoroutineExceptionHandler placed on a child launch have no effect, while the same handler on the scope works?

level: middleimportance: must knowfreq 60%

basics

~20 s

Child coroutines do not handle their own failures; they pass them up to the parent. So a handler set on a child is ignored. Only the handler on the root coroutine or scope, where failures finally land, actually runs.

open as a page

Show why a CoroutineExceptionHandler does not catch an exception from async, and where that exception must be handled instead.

level: seniorimportance: must knowfreq 50%

basics

~20 s

async stores its failure inside the returned Deferred. The handler only catches failures that nobody is expected to observe. Since you are expected to call await on a Deferred, the exception is rethrown there, and you handle it with try/catch around await.

open as a page

When should you use a CoroutineExceptionHandler versus a try/catch inside the coroutine, and what can each NOT do?

level: middleimportance: should knowfreq 55%

basics

~20 s

Use try/catch to handle a known error where it happens and keep going. Use the handler as a global safety net for unexpected failures of fire-and-forget work. The handler cannot recover the coroutine; try/catch cannot catch failures from sibling coroutines.

open as a page

On which thread/context is CoroutineExceptionHandler invoked, and how does it behave when multiple sibling coroutines fail?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The handler runs as part of the failing coroutine's machinery, using its context. If several children fail close together, only the first exception is reported to the handler and the others are attached to it as suppressed exceptions, so you do not lose them.

open as a page