skip to content

Operator placement matters: how does the position of `catch` relative to other operators (`map`, `onEach`) change which exceptions it handles?

level: middleimportance: should knowfreq 45%

answer

  1. Scope = everything above the catch
  2. catch-after-map catches map; catch-before-map doesn't
  3. Upstream terminates on first error
  4. Fallbacks flow into downstream operators
  5. Put catch just above collect for whole-pipeline coverage

basics

~20 s

catch only handles errors from the operators written above it. If you put it before a map, it won't catch errors the map throws. Put it after the operators whose errors you want to handle.

solid answer

~40 s

Because `catch` is upstream-only, its position in the chain defines its scope: it intercepts exceptions from the builder and every operator declared *above* it, but nothing below. So `flow.map { risky() }.catch { }` catches a throw inside `map`, while `flow.catch { }.map { risky() }` does not — the `map` is now downstream of `catch`. The same applies to `onEach`, `filter`, `transform`, etc. A common pattern is to place a single `catch` just above the terminal `collect` to cover the entire pipeline, or to place narrower `catch` blocks to recover at specific stages. Note: a `catch` can also emit fallbacks that subsequent downstream operators will then process, so placing operators after a `catch` lets you post-process the fallback values too.

code

kotlin · 7 lines
kotlin
flowOf(1, 2, 3)
    .map { check(it != 2) { "bad" }; it }
    .catch { emit(-1) }   // catches map error
    .map { it * 100 }     // runs on fallback too
    .collect(::println)
// 100
// -100

go deeper

for a junior

Knows catch covers operators above it.

for a middle

Correctly predicts catch-before vs catch-after-map behavior and that fallbacks flow downstream.

for a senior

Explains upstream termination on first error and structural patterns to continue past errors (per-item sub-flows).

for a principal

Designs layered catch placement for partial recovery and reasons about observability of each stage.

## The rule, restated `catch` sees only **upstream** exceptions — those produced by operators *above* it in the declared chain. Operators below it are out of scope. ## Same `map`, different placement ```kotlin // (A) catch AFTER map -> catches map's failure flowOf(1, 2, 3) .map { if (it == 2) error("bad") else it } .catch { emit(-1) } .collect { println(it) } // 1, -1 (stops upstream after the error, emits fallback) // (B) catch BEFORE map -> does NOT catch map's failure flowOf(1, 2, 3) .catch { emit(-1) } // only sees the builder/flowOf, which never throws .map { if (it == 2) error("bad") else it } .collect { println(it) } // 1, then exception thrown from collect ``` In (A) the `map` is upstream of `catch`, so its `error("bad")` is caught. In (B) the `map` is downstream, so its exception is invisible to `catch` and propagates to the terminal operator. ## Flow terminates on the first upstream error When an upstream exception reaches `catch`, the upstream flow is already cancelled/finished — you can't 'resume' it to get the remaining elements. That's why (A) prints `1, -1` and not `1, -1, 3`. `catch` is a recovery/terminate point, not a per-element skip. (For continuing past errors you'd restructure, e.g. catch *inside* a per-item sub-flow with `flatMapMerge`.) ## Post-processing fallbacks Fallbacks emitted from `catch` flow into *downstream* operators: ```kotlin risky .catch { emit(0) } .map { it * 10 } // also runs on the fallback 0 -> 0 .collect(::println) ``` ## Multiple catches You can chain several `catch` blocks; each handles failures from its own upstream, and a later `catch` can also handle exceptions *rethrown* by an earlier one. ## Key APIs - `Flow.catch`, `Flow.map`, `Flow.onEach`, `Flow.filter`, `Flow.transform` — all intermediate; declaration order = stream order. - Inside `catch`: `emit` / `emitAll` feed downstream operators.

  • If you want to handle errors from BOTH a `map` and the builder, where does `catch` go?
    Below both — i.e. after the `map` — so they are both upstream of the single `catch`.
  • Will `catch` let the flow continue emitting remaining upstream elements after an error?
    No. The first upstream exception terminates the upstream; `catch` only runs once at that point and can emit fallbacks, not resume the original stream.

saying these in an interview costs you the question

  • Placing `catch` before the operator that throws and expecting it to catch
  • Thinking `catch` resumes/skips and continues the original upstream
  • Believing one `catch` at the top covers downstream operators
  • Not realizing fallbacks are post-processed by downstream operators

context