skip to content

How do exceptions and cancellation behave in a flow { } producer, and what is the 'exception transparency' principle as it relates to emit/collect?

level: middleimportance: should knowfreq 50%

answer

  1. Producer runs in collector's coroutine — shared fate
  2. Producer throw => collect rethrows, flow terminates
  3. Exception transparency: don't swallow around emit
  4. catch = upstream only; onCompletion observes all
  5. Let CancellationException propagate; use try/finally

basics

~10 s

If 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 s

A 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 lines
kotlin
val 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

for a junior

Knows an error in the block surfaces at collect; may not know the right operators.

for a middle

Uses catch/onCompletion correctly and understands upstream-only semantics and cancellation cleanup.

for a senior

Explains exception transparency precisely and why swallowing around emit is a violation, including the cancellation hazard.

for a principal

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

context