skip to content

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

level: middleimportance: should knowfreq 55%

answer

  1. try/catch = local recovery; handler = global safety net
  2. Handler observes, never recovers
  3. try/catch can't catch sibling-coroutine failures
  4. Always rethrow CancellationException
  5. Wrapping launch{} doesn't catch its later failure

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.

solid answer

~40 s

`try/catch` is **structured, local** handling: it wraps a specific suspending call, lets you recover (fallback, retry, return a default) and continue. It only catches what is thrown on that call stack — not failures originating in other coroutines. `CoroutineExceptionHandler` is a **global, last-resort** hook for *uncaught* exceptions from `launch` at the root: it is ideal for logging/alerting/metrics, not recovery — once it runs, the coroutine is already failed and cannot resume. Caveat: when launching children via `coroutineScope { ... }` or nested `launch`, an exception that propagates structurally may bypass a naive try/catch you placed around the wrong thing; you must wrap the actual failing call. Also: catching `CancellationException` and swallowing it breaks cooperative cancellation — rethrow it.

go deeper

for a junior

Knows try/catch handles local errors and the handler is a global net.

for a middle

Explains recovery vs reporting and the CancellationException rethrow rule.

for a senior

Knows the launch{} wrapping gotcha and where each tool applies across async/coroutineScope.

for a principal

Sets team conventions: local handling for recoverable paths, a single root handler for observability, plus cancellation hygiene.

## Two different jobs | Aspect | try/catch | CoroutineExceptionHandler | |---|---|---| | Scope | Local, around a specific call | Global, last-resort for a scope/root | | Can recover/continue? | Yes | No (coroutine already failed) | | Catches sibling-coroutine failures? | No | Yes, if they reach the root as uncaught | | Typical use | Fallback, retry, default value | Logging, metrics, alerting | | Applies to async? | Yes (around await) | No | ## try/catch: local recovery ```kotlin suspend fun load(): Data = try { api.fetch() } catch (e: IOException) { cache.read() // recover and continue } ``` This only catches exceptions thrown by `api.fetch()` on this stack. It cannot catch a failure in a different coroutine. ## Handler: global safety net ```kotlin val handler = CoroutineExceptionHandler { _, e -> logger.error("unhandled", e) } val scope = CoroutineScope(SupervisorJob() + handler) scope.launch { riskyFireAndForget() } // failures land in the logger ``` It cannot turn the failure into a success; it observes and reports. ## The CancellationException trap A broad `catch (e: Exception)` will also catch `CancellationException`, which is how coroutines signal cancellation. Swallowing it makes a coroutine refuse to cancel. Always rethrow: ```kotlin try { work() } catch (e: CancellationException) { throw e } catch (e: Exception) { handle(e) } ``` ## Gotcha with structured builders Wrapping a `coroutineScope { launch { ... } }` in try/catch can catch the rethrown failure, but wrapping just the `launch` call does **not** catch the child's later failure, because `launch` returns immediately. Wrap `await()` for async, or the `coroutineScope`/`supervisorScope` block for grouped children. Key APIs/keywords: `try/catch`, `CoroutineExceptionHandler`, `CancellationException`, `coroutineScope`, `supervisorScope`, `await`, `launch`.

  • Why is catching and swallowing CancellationException dangerous?
    Cancellation is signalled by throwing CancellationException; swallowing it stops the coroutine from cooperatively cancelling, leaking work. Rethrow it after any cleanup.
  • Does wrapping a launch call in try/catch catch the coroutine's exception?
    No. launch returns immediately, so the try block has already exited before the body fails. Wrap the actual suspending call inside the coroutine, or the coroutineScope block.

saying these in an interview costs you the question

  • Using the handler to 'retry' or recover a coroutine
  • Swallowing CancellationException in a broad catch
  • Expecting try/catch around launch{} to catch its body's failure
  • Thinking try/catch can catch failures from other coroutines

context