How do retry/retryWhen interact with CancellationException and exception transparency? What surprises engineers?
answer
- CancellationException is never retried — control flow, not error
- Retry handles upstream failures only (exception transparency)
- Downstream/collect throws escape, not retried
- Side effects in flow { } repeat per attempt
- try/catch around emit => 'exception transparency violated'
basics
~10 sRetry 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.
solid answer
~40 sretry/retryWhen deliberately skip CancellationException: even if your predicate returns true, a cancelled coroutine stops, because cancellation must propagate cooperatively. Internally the retry machinery checks for cancellation before re-subscribing. Second, retry honors EXCEPTION TRANSPARENCY: an operator may only react to exceptions from its upstream. So retry catches failures from operators above it; exceptions thrown downstream (e.g. inside collect or a downstream map) are not retried and escape normally. A classic surprise: putting business logic that can throw INSIDE the upstream flow { } means retry will re-run it, but a throw inside collect won't be retried. Another: emitting from a wrong context or catching-then-re-emitting can violate transparency and throw IllegalStateException — retry/catch must not be used to mask that.
go deeper
Knows cancellation stops the flow and retry deals with errors.
Explains that retry only restarts on upstream failures and downstream throws escape.
Articulates exception transparency, the CancellationException exclusion, and diagnoses the transparency-violation error.
Explains why the boundary exists for composability and structured concurrency, and reviews pipelines for misplaced retry/catch.
## CancellationException is special Kotlin coroutines use **cooperative cancellation**: when a coroutine's `Job` is cancelled, suspending functions throw `CancellationException` to unwind. This exception is **control flow, not an error**. Every Flow retry operator excludes it: - Even if your `retryWhen` predicate would return `true` for the cause, a genuine cancellation is not retried — the flow stops. - Practically, never write `catch`/`retryWhen` logic that treats `CancellationException` as retryable; doing so can break structured concurrency and leak coroutines. ```kotlin upstream.retryWhen { cause, _ -> cause is IOException // CancellationException simply isn't retried } ``` ## Exception transparency **Exception transparency** is the Flow rule that an operator only handles failures of its **upstream**, and a `flow { }` builder must not emit from inside a `try/catch` that swallows downstream exceptions. Consequences for retry: - `retry` re-subscribes only when the **upstream** throws. Anything **downstream** of `retry` is out of scope. ```kotlin flow { emit(load()) } // (A) upstream — retried .map { transform(it) } // (B) upstream of retry — retried .retry(3) .collect { render(it) } // (C) downstream — NOT retried ``` If `render` throws, `retry` never sees it; the exception propagates to the caller of `collect`. ## The classic surprises 1. **"My retry isn't firing."** The failure is downstream of `retry` (in `collect` or a `map` placed after `retry`). Move `retry` below the failing step. 2. **"Why did it run my side effect 4 times?"** Because the side effect is in the upstream `flow { }` block, which re-executes per attempt. 3. **IllegalStateException: Flow exception transparency is violated.** Thrown when a `flow { }` builder catches an exception around its `emit` and continues, or emits from the wrong coroutine context. `retry`/`catch` are not a license to do this — fix the producer to use the proper operators instead of try/catch around emit. ## Why the design is this way Limiting operators to upstream failures keeps pipelines **composable and predictable**: a downstream consumer's bug can't be silently re-run by an unrelated upstream operator, and cancellation always means stop. This is the same boundary that the `catch` operator enforces.
- If an exception is thrown inside collect, will retry re-subscribe?No. collect is downstream of retry; retry only reacts to upstream failures, so the exception escapes.
- What triggers 'Flow exception transparency is violated'?Catching an exception around emit in a flow { } builder and continuing, or emitting from the wrong context; use catch/retry operators instead of try/catch around emit.
A referee can only review plays from before the whistle (upstream); and if the game is cancelled, no review restarts it.
saying these in an interview costs you the question
- Claiming retry can retry a cancelled coroutine
- Expecting retry to catch errors from collect
- Wrapping emit in try/catch to 'help' retry
- Not knowing CancellationException is control flow
- Believing transparency is a bug rather than a guarantee