Contrast onCompletion with the catch operator and with a try/finally inside a flow { } builder. Which can handle exceptions, which only observe, and which run in the collector's context?
answer
- catch = handle (upstream only)
- onCompletion = observe + rethrow
- try/finally in flow{} = producer context
- catch before onCompletion => cause null
- exception transparency = downstream-only propagation
basics
~10 scatch handles and stops upstream errors. onCompletion only watches the ending and rethrows. A try/finally inside flow { } runs in the producer and cleans up there. Use onCompletion to observe, catch to recover.
solid answer
~50 scatch is a recovery operator: it intercepts exceptions from upstream only, can swallow them or emit fallback values, and respects exception transparency (it won't catch downstream errors). onCompletion is observational: its cause parameter exposes how the flow ended, but it always rethrows that cause, so it cannot recover. A try/finally written inside a flow { } builder runs in the producer's context (the upstream/flowOn dispatcher) and is ideal for releasing resources the producer owns, but it does not see downstream cancellation in the same convenient way. onStart/onCompletion run in the collector's context. Order matters: place catch after onCompletion if you want onCompletion to still observe the real failure cause; place catch before onCompletion if you want failures already handled (cause becomes null). Exception transparency means you must not throw from inside the producer's emit path in ways that violate the contract; catch enforces upstream-only scope.
code
kotlin · 4 linesflow { error("boom") }
.onCompletion { cause -> println("observed: $cause") } // observed: boom (rethrows)
.catch { e -> emit(-1) } // recovers, emits fallback
.collect { println(it) } // observed: boom, then -1go deeper
Knows catch handles errors and onCompletion just observes the ending.
Explains onCompletion rethrows while catch can emit fallbacks and stop propagation.
Maps all three tools to context (producer vs collector) and explains exception transparency and ordering effects.
Designs error/cleanup strategies across boundaries, reasoning about flowOn context shifts, transparency violations, and operator-order trade-offs.
## Three tools, three jobs ### 1. `catch { e -> ... }` — recovery (handles) ```kotlin upstream .catch { e -> emit(fallback) } // can swallow or emit, stops propagation ``` - Catches exceptions from **upstream only** (enforcing *exception transparency*). - May **emit()** replacement values, rethrow, or do nothing (swallowing the error). - Will **not** catch exceptions thrown by the downstream collector — those belong to the consumer. ### 2. `onCompletion { cause -> ... }` — observation (does NOT handle) ```kotlin upstream .onCompletion { cause -> /* inspect cause, cleanup */ } // always rethrows cause ``` - Sees the terminating `cause` (null / CancellationException / failure) but **rethrows** it; it is finally-like, not catch-like. - Runs in the **collector's context**. ### 3. `try/finally` inside `flow { }` — producer-side cleanup ```kotlin flow { val resource = open() try { emit(resource.read()) } finally { resource.close() // runs in the PRODUCER context (e.g. set by flowOn) } } ``` - Lives inside the producer lambda, so `finally` runs in the **producer's context** (which `flowOn` may shift to a different dispatcher). - Best for resources the producer owns (file handles, DB cursors). ## Context summary | Mechanism | Handles exceptions? | Can emit? | Runs in | |---|---|---|---| | `catch` | Yes (upstream only) | Yes | collector context | | `onCompletion` | No (observes + rethrows) | Yes | collector context | | `try/finally` in `flow {}` | finally cleans up, try/catch can handle locally | n/a | producer context (per flowOn) | ## Exception transparency Exceptions must propagate **downstream** through operators, and a flow must not catch exceptions that occur downstream of itself. `catch` formalizes this: it only intercepts upstream failures. Violating transparency (e.g. swallowing in a try/catch around `emit`) breaks the contract and can hide cancellation. ## Ordering: how catch and onCompletion interact ```kotlin flow { throw IOException() } .onCompletion { c -> log("A cause=$c") } // sees IOException (failure) .catch { log("handled") } // handles after observation .collect() // vs swapping them: flow { throw IOException() } .catch { log("handled") } // failure handled first .onCompletion { c -> log("B cause=$c") } // sees null (normal completion now) .collect() ``` Choose the order based on whether onCompletion should observe the *raw* failure or the *post-recovery* outcome.
- Can catch handle an exception thrown by the collect { } block?No. catch only intercepts upstream exceptions; errors from the downstream collector are outside its scope by the exception-transparency contract.
- Where does finally run for a try/finally written inside a flow { } builder?In the producer's context — the dispatcher set by any upstream flowOn — not necessarily the collector's context.
saying these in an interview costs you the question
- Claiming onCompletion can recover from or swallow an exception
- Saying catch handles downstream collector errors
- Ignoring that catch placement changes onCompletion's observed cause
- Assuming try/finally in flow{} runs in the collector context
- Not knowing exception transparency restricts catch to upstream