Explain the principle of "exception transparency" in Kotlin Flow. Why is wrapping `emit` in a try/catch a violation?
answer
- Flow exceptions are transparent to the caller
- Never try/catch around `emit`
- `emit` resumption carries downstream errors back
- Use `catch {}` (upstream-only) instead
- Compute-in-try, emit-outside pattern
basics
~20 sException 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 sException 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// 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-onlygo deeper
Knows you shouldn't try/catch around emit and should use catch instead.
Explains that emit's resumption carries downstream exceptions back, which is why the wrap is illegal.
Names the runtime enforcement (IllegalStateException), and gives the compute-in-try/emit-outside fix.
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