skip to content

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

level: seniorimportance: should knowfreq 33%

answer

  1. CancellationException is never retried — control flow, not error
  2. Retry handles upstream failures only (exception transparency)
  3. Downstream/collect throws escape, not retried
  4. Side effects in flow { } repeat per attempt
  5. try/catch around emit => 'exception transparency violated'

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.

solid answer

~40 s

retry/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

for a junior

Knows cancellation stops the flow and retry deals with errors.

for a middle

Explains that retry only restarts on upstream failures and downstream throws escape.

for a senior

Articulates exception transparency, the CancellationException exclusion, and diagnoses the transparency-violation error.

for a principal

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

context