skip to content

Error & Completion

The lifecycle side of Flow: hooks that run at start and completion, the catch operator and the exception-transparency rule behind it, and retry. Failure handling is where Flow code most often goes wrong in production.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

15

What does the `catch { }` operator do in a Kotlin Flow, and which exceptions can it handle?

level: juniorimportance: must knowfreq 70%

answer

  1. Intermediate operator, upstream-only
  2. Lambda receives Throwable (`it`)
  3. Can emit/emitAll a fallback
  4. Does NOT catch the collector body
  5. Place just above collect

basics

~10 s

catch runs when something fails earlier in the flow (upstream). It only catches errors from the parts above it, lets you log them, and can send a backup value instead of crashing.

solid answer

~40 s

`catch { }` is an intermediate Flow operator that intercepts exceptions thrown by the upstream — the flow builder and all operators declared *above* it. Inside its lambda the thrown value is bound to `it` (type `Throwable`). It cannot see exceptions from operators *below* it, including the terminal `collect`. From the lambda you may log, swallow, rethrow, or call `emit(...)` / `emitAll(...)` to provide a fallback value so collection continues normally. Errors from downstream code (e.g. the collector body) are NOT caught here — that is the exception-transparency rule. Typical use: put `catch` just above `collect` to handle a failing network/database flow and emit a default.

code

kotlin · 8 lines
kotlin
flow {
    emit("a")
    throw IllegalStateException("fail")
}
    .catch { e -> emit("fallback:${e.message}") }
    .collect { println(it) }
// a
// fallback:fail

go deeper

for a junior

Knows catch handles upstream errors and can emit a fallback.

for a middle

Explains upstream-only scope and that the collector body is excluded.

for a senior

Ties it to exception transparency and explains why downstream is intentionally excluded.

for a principal

Frames placement of catch as a design choice and discusses fallback vs propagate strategies for production flows.

## What `catch` is `catch { }` is an *intermediate operator* on `Flow`. Intermediate means it sits in the chain between the source (the `flow { }` builder or `flowOf(...)`) and the *terminal* operator (`collect`, `toList`, `first`, ...). It returns a new `Flow` and does nothing until collected. ## Upstream vs downstream - **Upstream** = everything declared *before* (above) `catch` in the chain. - **Downstream** = everything *after* it, including the terminal `collect` and the collector's lambda body. `catch` only sees exceptions that come from **upstream**. This is deliberate. ```kotlin flow { emit(1) throw RuntimeException("boom") // upstream failure } .catch { e -> println("caught: ${e.message}"); emit(-1) } // sees boom, emits fallback .collect { println(it) } // prints: 1, caught: boom, -1 ``` ## What you can do inside `catch` The lambda receives the `Throwable` as `it` (or a named param). You can: - **Log / swallow** it (do nothing else) — the flow then completes normally. - **`emit(value)` or `emitAll(otherFlow)`** — emit one or more *fallback* values downstream. - **Rethrow** (`throw it`) or throw a different exception to propagate failure. ```kotlin dataFlow .catch { emit(emptyList()) } // fallback to empty list on any upstream error .collect { render(it) } ``` ## What it does NOT catch It does **not** catch exceptions thrown in the collector or in any operator below it: ```kotlin flowOf(1, 2) .catch { println("never here") } .collect { throw RuntimeException("in collector") } // propagates to caller, NOT to catch ``` This is **exception transparency**: a `catch` only ever reacts to upstream emissions failing, never to how downstream consumes them. ## Key APIs - `Flow.catch(action: suspend FlowCollector<T>.(Throwable) -> Unit)` — the operator. - Inside it `emit` / `emitAll` are available because the receiver is a `FlowCollector<T>`.

  • If you remove the `catch` and the upstream throws, what happens?
    The exception propagates down to the terminal operator and is thrown from the `collect` call, so the coroutine collecting it fails unless wrapped in try/catch.
  • Can `catch` re-emit multiple values?
    Yes — its receiver is a `FlowCollector`, so you can call `emit` several times or `emitAll(anotherFlow)`.

Like a safety net strung under a tightrope: it only catches falls that happen above it, not the people walking on the ground below.

saying these in an interview costs you the question

  • Saying `catch` catches errors from the collector / downstream
  • Claiming `catch` is a terminal operator
  • Thinking you can't emit fallbacks from `catch`
  • Confusing it with a plain try/catch wrapping the whole chain

context

open as a page

What do the Flow operators onStart and onCompletion do, and when does each block run?

level: juniorimportance: must knowfreq 70%

basics

~10 s

onStart runs once right before a flow starts producing values. onCompletion runs once after the flow finishes, whether it ended normally, was cancelled, or threw an error.

open as a page

What does the Flow operator retry(count) do, and where does it sit in a Flow pipeline?

level: juniorimportance: must knowfreq 62%

basics

~10 s

retry re-subscribes to the flow when it fails with an error, trying again up to the given number of times before letting the error through.

open as a page

Explain the principle of "exception transparency" in Kotlin Flow. Why is wrapping `emit` in a try/catch a violation?

level: middleimportance: must knowfreq 55%

basics

~20 s

Exception 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.

open as a page

How does retryWhen { cause, attempt -> } work, and what do its parameters mean?

level: middleimportance: must knowfreq 55%

basics

~10 s

retryWhen runs a function each time the flow fails. You get the error and how many times it has already retried, and you return true to try again or false to give up.

open as a page

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%

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.

open as a page

Explain the cause parameter of onCompletion. What are its possible values and how do you distinguish normal, cancelled, and failed completion?

level: middleimportance: should knowfreq 60%

basics

~10 s

The cause is a nullable error. It is null when the flow finished fine, a cancellation error when the collector was cancelled, and the thrown exception when the flow failed.

open as a page

Show how onStart and onCompletion are used to drive a loading/idle UI state, and why this is preferred over try/finally around collect.

level: middleimportance: should knowfreq 55%

basics

~10 s

Use onStart to switch the UI to loading before data arrives, and onCompletion to switch it back to done after the flow ends. They keep show/hide symmetric and tied to that one flow.

open as a page

Why does retry require the upstream Flow to be cold/restartable, and what happens if you retry a hot or non-idempotent source?

level: middleimportance: should knowfreq 28%

basics

~20 s

Retry works by starting the flow over from the beginning. That only makes sense for a cold flow that re-runs cleanly. With a hot or one-shot source, restarting may do nothing useful or repeat real side effects.

open as a page

Inside a `catch { }` block, what are your options for the caught throwable, and how do you selectively handle only certain exception types?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Inside catch you can ignore the error, log it, send a backup value with emit, or rethrow it. To handle only some error types, check the type and rethrow the rest.

open as a page

When should you use the `catch { }` operator versus wrapping the `collect` call in a plain `try/catch`? What does each cover?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Use catch for errors coming from the flow itself (upstream). Use a try/catch around collect when you also need to handle errors from your collector code. They cover different parts.

open as a page

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?

level: seniorimportance: should knowfreq 45%

basics

~10 s

catch 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.

open as a page

How do retry/retryWhen interact with CancellationException and exception transparency? What surprises engineers?

level: seniorimportance: should knowfreq 33%

basics

~10 s

Retry never restarts a flow that was cancelled — cancellation always wins. And retry only restarts the flow for errors that came from above it, not from your collector below.

open as a page

Implement exponential backoff with jitter for a flaky network Flow using retryWhen, retrying only transient errors. Walk through the design.

level: seniorimportance: should knowfreq 45%

basics

~10 s

Use retryWhen: check the error is retryable and you haven't hit the cap, wait a growing amount of time (doubling each try) plus a small random delay, then retry; otherwise give up.

open as a page

What are the subtle pitfalls of onStart and onCompletion: emitting after failure, exception transparency violations, and ordering with flowOn and catch?

level: principalimportance: nice to knowfreq 25%

basics

~10 s

Don't emit from onCompletion after a failure, don't treat onCompletion as a place to swallow errors, and remember catch and flowOn ordering changes what onCompletion sees and which thread it runs on.

open as a page