How do exceptions and cancellation behave in a flow { } producer, and what is the 'exception transparency' principle as it relates to emit/collect?
answer
- Producer runs in collector's coroutine — shared fate
- Producer throw => collect rethrows, flow terminates
- Exception transparency: don't swallow around emit
- catch = upstream only; onCompletion observes all
- Let CancellationException propagate; use try/finally
basics
~10 sIf the producer block throws, collect rethrows that exception and the flow stops. You should not wrap emit in a try/catch that swallows errors. Cancelling the collector cancels the producer too.
solid answer
~40 sA flow { } producer and its collector form one cooperative pipeline. If the block throws (or a suspend call fails), the exception propagates down and collect rethrows it; the flow terminates. Exception transparency means a flow must not catch exceptions thrown downstream (in the collector or via emit) and pretend it succeeded — wrapping emit in try/catch that swallows is a violation and can hide CancellationException. The correct tools are the catch operator (handles only upstream exceptions, never downstream/collector ones) and onCompletion. Cancellation: collect runs in the collector's coroutine, so cancelling that coroutine cancels the producer; emit and delay are cancellable suspend points that throw CancellationException, which must propagate (don't swallow it). Use try/finally in the block for cleanup; it runs on cancellation.
code
kotlin · 12 linesval f = flow {
try {
emit(1)
emit(2)
} finally {
println("cleanup") // runs even on cancellation
}
}
suspend fun run() {
f.take(1).collect { println(it) } // prints 1, then cleanup (cancelled after first)
}go deeper
Knows an error in the block surfaces at collect; may not know the right operators.
Uses catch/onCompletion correctly and understands upstream-only semantics and cancellation cleanup.
Explains exception transparency precisely and why swallowing around emit is a violation, including the cancellation hazard.
Designs robust pipelines (retry/catch placement, resource lifecycle) and reasons about structured-concurrency guarantees across operators.
## One cooperative pipeline When you `collect`, the `flow { }` block runs in the collector's coroutine. So producer and consumer share a fate: an exception or cancellation on either side tears down the whole pipeline. ## Exception propagation If the producer throws: ```kotlin flow { emit(1) throw IllegalStateException("boom") }.collect { println(it) } // prints 1, then collect rethrows IllegalStateException ``` The exception travels downstream and is rethrown by the terminal `collect`. The flow is now terminated. ## Exception transparency **Exception transparency** is the rule that a flow may not emit values that come from catching a downstream exception. Concretely: do NOT wrap `emit` in a `try/catch` that swallows the error: ```kotlin // ANTI-PATTERN — violates exception transparency flow { try { emit(1) // if the COLLECTOR throws, you'd catch it here } catch (e: Exception) { emit(-1) // wrong: hides downstream failures, may swallow cancellation } } ``` The right tools: - **`catch { }`** — an intermediate operator that catches **only upstream** exceptions (from emitters above it), never exceptions from the collector below. Inside it you can `emit` a fallback. - **`onCompletion { cause -> }`** — runs on normal completion, failure, or cancellation; `cause` is non-null on failure/cancel. Cannot swallow the exception (it still propagates). ```kotlin upstream .map { risky(it) } .catch { e -> emit(fallback) } // only upstream errors .collect { use(it) } ``` ## Cancellation `emit`, `delay`, and other suspend points are **cancellable**. If the collector's coroutine is cancelled (e.g. `withTimeout`, scope cancelled, `take(n)` reached its limit), the next suspend point throws `CancellationException`, which unwinds the producer. - Never catch and swallow `CancellationException` — it must propagate or coroutines break. A bare `catch (e: Exception)` around `emit` is dangerous because it also catches cancellation. Prefer `try/finally` for cleanup, or check the specific exception type. ```kotlin flow { try { while (true) { emit(next()); delay(100) } } finally { close() // runs on cancellation/completion } } ``` ## Summary of tools - `catch` — upstream-only recovery. - `onCompletion` — observe completion/failure/cancel. - `try/finally` — resource cleanup inside the block. - Let `CancellationException` propagate.
- Why does catch { } not handle exceptions thrown inside the collect { } lambda?catch only intercepts exceptions coming from upstream emitters. Downstream/collector exceptions are not upstream, so they bypass catch and are rethrown by the terminal operator. This preserves exception transparency.
- What happens to the producer when the collector reaches take(2) on an infinite flow?take cancels the upstream after the 2nd value; the next emit/suspend point throws CancellationException, the producer's try/finally runs, and collect completes normally.
saying these in an interview costs you the question
- Wraps emit in try/catch (Exception) and swallows errors
- Catches and ignores CancellationException
- Thinks catch { } handles collector-side exceptions
- Believes an exception in the producer is silently dropped