skip to content

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

level: middleimportance: must knowfreq 55%

answer

  1. predicate: suspend FlowCollector<T>.(cause, attempt) -> Boolean
  2. attempt is zero-based (0 on first failure)
  3. true = retry, false = re-throw cause
  4. suspend => can delay() for backoff; receiver => can emit()
  5. retry(n) is retryWhen { _, a -> a < n }

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.

solid answer

~40 s

retryWhen is the general form of retry. Its signature is retryWhen(predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean). The predicate is a SUSPEND lambda with FlowCollector<T> as receiver, called whenever the upstream throws. `cause` is the exception; `attempt` is the zero-based count of retries already done (0 on the first failure). Returning true re-subscribes to the upstream; returning false re-throws `cause` downstream. Because it is suspend, you can call delay() inside for backoff. Because the receiver is FlowCollector<T>, you may emit() a fallback value before deciding. retry(n) is implemented on top of retryWhen as `retryWhen { cause, attempt -> attempt < n && predicate(cause) }`. Like retry, it never retries CancellationException.

code

kotlin · 7 lines
kotlin
upstream.retryWhen { cause, attempt ->
    when {
        cause !is IOException -> false        // only retry IO errors
        attempt >= 3          -> false        // cap at 3 retries
        else -> { delay(200); true }          // backoff then retry
    }
}

go deeper

for a junior

Knows retryWhen decides whether to retry using the error.

for a middle

Explains the zero-based attempt counter and the true/false semantics correctly.

for a senior

Leverages the suspend + FlowCollector receiver for backoff and fallback emission, and relates it to retry.

for a principal

Designs retry policies (which exceptions are retryable, caps, jitter) and reasons about idempotency and downstream load.

## Signature and parameters ```kotlin fun <T> Flow<T>.retryWhen( predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean ): Flow<T> ``` `retryWhen` is the **most general** retry operator; `retry` is a thin wrapper over it. - **`cause: Throwable`** — the exception the upstream threw on this failure. - **`attempt: Long`** — how many retries have ALREADY happened. It is **zero-based**: it is `0` the first time the flow fails, `1` after one retry, and so on. So `attempt` equals the number of completed retries, not the attempt number you are about to make. - **Return value** — `true` re-subscribes to (re-collects) the upstream; `false` re-throws `cause` downstream, ending the flow with that exception. ## It is a suspend lambda with a FlowCollector receiver Two powerful consequences: 1. **You can suspend** — call `delay(...)` to implement backoff between attempts. 2. **`this` is `FlowCollector<T>`** — you can `emit(fallback)` a value before deciding whether to retry, e.g. emit a cached value then give up. ```kotlin flow { emit(api.load()) } .retryWhen { cause, attempt -> if (cause is IOException && attempt < 3) { delay(100L * (attempt + 1)) // linear backoff true // retry } else { false // give up; cause propagates } } .collect { println(it) } ``` ## Relationship to retry Conceptually: ```kotlin fun <T> Flow<T>.retry(retries: Long, predicate: suspend (Throwable) -> Boolean) = retryWhen { cause, attempt -> attempt < retries && predicate(cause) } ``` So whenever you need **conditional logic, backoff delays, jitter, or a fallback emit**, reach for `retryWhen`; when you just need a fixed cap on any error, `retry(n)` reads cleaner. ## Cancellation Like all retry operators, `retryWhen` does **not** retry `CancellationException`. Even if your predicate returns `true`, cancellation propagates so the coroutine stops. Avoid writing predicates that try to swallow cancellation. ## Common bug: off-by-one on attempt Because `attempt` is zero-based, `attempt < 3` allows attempts when `attempt` is 0, 1, 2 — i.e. **3 retries** (4 total runs). Using `attempt <= 3` would give 4 retries.

  • What value does attempt hold the very first time the flow fails?
    0 — it counts retries already performed, so the first failure sees attempt == 0.
  • Why can you call delay() inside the retryWhen predicate?
    Because the predicate is a suspend function, so suspension points like delay are allowed for backoff.

A bouncer at the door who, each time you're turned away, checks why and how many times you've tried before deciding to let you queue again.

saying these in an interview costs you the question

  • Saying attempt is one-based or equals the upcoming attempt number
  • Not knowing the predicate is suspend (claiming you can't delay)
  • Thinking you must return Unit instead of a Boolean
  • Believing returning true ignores cancellation
  • Confusing FlowCollector receiver — not knowing you can emit a fallback

context