skip to content

Explain the principle of "exception transparency" in Kotlin Flow. Why is wrapping `emit` in a try/catch a violation?

level: middleimportance: must knowfreq 55%

answer

  1. Flow exceptions are transparent to the caller
  2. Never try/catch around `emit`
  3. `emit` resumption carries downstream errors back
  4. Use `catch {}` (upstream-only) instead
  5. Compute-in-try, emit-outside pattern

basics

~20 s

Exception transparency means a flow must let downstream errors flow back out, not swallow them. So you should never put a try/catch around emit, because that would accidentally hide errors that really belong to the collector.

solid answer

~50 s

Exception transparency is the Flow contract that a flow's exceptions are *transparent* to its callers: a flow may only catch exceptions that come from *itself* (upstream), and must never catch exceptions thrown by the downstream collector. The danger is that `emit` resumes the downstream — if the collector throws, that exception travels *back up through* the `emit` call. So a `try/catch` around `emit` inside a `flow { }` builder would catch the collector's exception, silently breaking transparency. Kotlin enforces this: the coroutines runtime can detect such violations and throw an `IllegalStateException` ("Flow exception transparency is violated") via internal checks. The correct, transparency-preserving tools are the `catch { }` operator (upstream-only) and `onCompletion { cause -> }` for observation. To genuinely guard a *block* of producer code, wrap the producing logic — not the `emit` call — or use `catch`.

code

kotlin · 10 lines
kotlin
// WRONG — violates transparency
flow {
    try { emit(1) } catch (e: Throwable) { emit(-1) }
}

// RIGHT — transparency preserved
flow {
    val v = try { risky() } catch (e: IOException) { -1 }
    emit(v)
}.catch { e -> emit(-2) } // handles upstream-only

go deeper

for a junior

Knows you shouldn't try/catch around emit and should use catch instead.

for a middle

Explains that emit's resumption carries downstream exceptions back, which is why the wrap is illegal.

for a senior

Names the runtime enforcement (IllegalStateException), and gives the compute-in-try/emit-outside fix.

for a principal

Connects transparency to the broader 'context preservation' guarantees and reasons about library API design that depends on it.

## The contract **Exception transparency** is a Flow design principle: exceptions thrown inside a flow must remain *visible* (transparent) to whoever collects it. Concretely, a flow implementation may catch and handle exceptions that originate from **upstream** (its own producing code), but it must **never** catch exceptions that come from **downstream** (the collector). ## Why `try/catch` around `emit` breaks it `emit` is a suspend function. When you call `emit(value)` inside a `flow { }` builder, control transfers to the downstream collector, runs the collector's lambda, and then **resumes** your `emit` call. Crucially, if the collector throws, that exception is delivered *back through the `emit` call site*. So: ```kotlin flow { try { emit(1) // collector may throw here } catch (e: Throwable) { // BUG: this can catch the COLLECTOR's exception, not just producer errors emit(-1) } } ``` This silently swallows a downstream error — exactly the violation. The runtime guards against it: collecting such a flow throws `IllegalStateException` with a message like *"Flow exception transparency is violated: Emission from another coroutine / previous emission ..."* in the related guarded cases. ## The transparency-preserving alternatives - **`catch { }`** — declarative, *upstream-only*. Equivalent in spirit to a try/catch around the whole upstream, but it can never see downstream exceptions. - **`onCompletion { cause -> }`** — observe the terminal cause (null = success) without consuming it. ```kotlin upstream .catch { e -> emit(fallback) } // legal: only upstream errors .collect { use(it) } ``` ## When you really need try/catch in a builder Wrap only the *producer* logic that can fail, and do not let the `emit` be inside the try if you want to preserve transparency. A cleaner pattern is to compute the value in a try/catch, then emit outside it: ```kotlin flow { val value = try { riskyLoad() } catch (e: IOException) { Fallback } emit(value) // emit is OUTSIDE the try } ``` ## Key APIs / keywords - `emit` / `emitAll` — suspend; resumption carries downstream exceptions back. - `catch { }` — upstream-only operator; receiver is `FlowCollector`. - `onCompletion { cause -> }` — observe completion/cause. - The runtime check throwing `IllegalStateException` ("exception transparency is violated").

  • What exception does the runtime throw when transparency is violated?
    An `IllegalStateException` whose message states that Flow exception transparency is violated, raised from the coroutines internals when an emission/exception is handled improperly.
  • Does `catch { }` violate transparency?
    No — `catch` is built to only intercept upstream exceptions and structurally cannot see downstream ones, so it respects the principle.

Like a relay runner: if you wrap your hand-off in a 'catch any dropped baton' clause, you might accidentally cover for the next runner's mistakes and hide them from the coach.

saying these in an interview costs you the question

  • Saying it's fine to try/catch around `emit`
  • Not realizing `emit` resumption brings downstream errors back upstream
  • Confusing exception transparency with thread safety / context preservation
  • Claiming `catch` can swallow collector exceptions

context