In RxJS 7, how would you retry a flaky price endpoint with exponential backoff using retry's config, and what do count, delay and resetOnSuccess control?
answer
- a config object, not retryWhen
- delay can be a function
- retryCount starts at one
- the notifier decides: emit, error, complete
- reset the budget after success
basics
~10 sUse retry({ count, delay }) where delay is a function of (error, retryCount) returning timer(base * 2 ** (retryCount - 1)); count caps retries, and resetOnSuccess restarts the count after the source emits again.
solid answer
~40 sRxJS 7's `retry` takes a config: `count` is the maximum number of **retries** (default unlimited), `delay` is either milliseconds or a function `(error, retryCount) => ObservableInput`, and `resetOnSuccess` (default `false`) resets the counter whenever the resubscribed source emits a value. For backoff, the function returns `timer(500 * 2 ** (retryCount - 1))` — `retryCount` is 1 on the first retry — giving 500, 1000, 2000 ms, ideally with random jitter. The notifier returned by `delay` decides: when it **emits**, `retry` resubscribes; when it **errors**, that error ends the stream, which is how you give up early on a non-retryable error with `throwError(() => err)`; when it **completes without emitting**, the result completes silently. `retryWhen` is deprecated in favour of this `delay` option.
code
ts · 11 linesimport { EMPTY, retry, throwError } from 'rxjs';
let attempts = 0;
const flaky$ = throwError(() => new Error(`fail ${++attempts}`));
flaky$
.pipe(retry({ count: 2, delay: () => EMPTY }))
.subscribe({
error: (e) => console.log('error', e.message),
complete: () => console.log('completed silently'), // this runs
});go deeper
Recall that retry resubscribes to a failing source and that RxJS 7 retry accepts count and delay options.
Explain count as retries rather than attempts, delay as a number or a function returning a notifier, and resetOnSuccess's default of false.
Write backoff with jitter, refuse non-transient errors by erroring the notifier, avoid the EMPTY-completes trap, and use resetOnSuccess on long-lived streams.
Set retry budgets with the backend's capacity in mind: which errors are retryable, how retries from many clients add up, and where a circuit breaker should replace retries.
## The retry config in RxJS 7 `retry` resubscribes to its source when the source errors. RxJS 7 added a configuration object that covers what used to need `retryWhen`: | Option | Type | Default | Meaning | |---|---|---|---| | `count` | number | unlimited | maximum number of **retries** after the first attempt | | `delay` | number, or `(error, retryCount) => ObservableInput` | none (retry at once) | wait before each retry, or decide per error | | `resetOnSuccess` | boolean | `false` | reset the retry counter when the resubscribed source emits a value | `retry(3)` is shorthand for `retry({ count: 3 })`, and `retry()` with no argument retries forever. ## Exponential backoff for a flaky price endpoint A price endpoint sometimes answers with a transient failure (a timeout, a 503). Retrying at once hammers it while it is already struggling; retrying after growing pauses gives it time to recover. ```ts import { Observable, retry, throwError, timer } from 'rxjs'; interface Price { sku: string; amount: number } declare function fetchPrice(sku: string): Observable<Price>; // cold: each subscribe sends a request declare function isTransient(error: unknown): boolean; // e.g. timeouts and 5xx const price$ = fetchPrice('A-100').pipe( retry({ count: 4, delay: (error, retryCount) => { if (!isTransient(error)) { return throwError(() => error); // give up at once } const backoff = 500 * 2 ** (retryCount - 1); // 500, 1000, 2000, 4000 ms const jitter = Math.random() * 250; return timer(backoff + jitter); }, }), ); ``` How it runs: 1. The first subscription sends the request. It fails. 2. `retry` increments its counter and calls `delay(error, 1)`. The function returns `timer(~500)`. 3. When the timer **emits**, `retry` resubscribes to the source, which sends the request again. 4. Each further failure calls `delay` with `retryCount` 2, 3, 4, doubling the wait. 5. After the fourth retry fails, the count is exhausted and the last error goes downstream. With `count: 4`, the endpoint is called **at most five times**: the original attempt plus four retries. ## The notifier decides what happens When `delay` is a function, the observable it returns is a **notifier**: - **It emits** → `retry` resubscribes to the source. - **It errors** → that error is delivered downstream and retrying stops. Returning `throwError(() => error)` is the clean way to refuse to retry a non-transient error such as a 404 or a validation failure. - **It completes without emitting** → the result **completes** without error. Returning `EMPTY` to "give up" therefore turns a failure into a silent success — a classic trap. A number instead of a function (`delay: 1000`) is a fixed pause between retries. ## resetOnSuccess for long-lived streams For a one-shot request, the counter never needs resetting. For a **long-lived** stream — a price ticker that polls or stays connected for hours — five failures spread over a day would otherwise exhaust `count: 4` and kill the stream for good. With `resetOnSuccess: true`, the counter returns to zero whenever the resubscribed source emits a value, so the budget applies to **consecutive** failures only. The backoff restarts from the shortest delay as well, since `retryCount` restarts too. ## Things to know before retrying - **Retry only cold sources.** `retry` resubscribes; that re-runs the work only if subscribing starts it, as it does for a request observable. - **Values from failed attempts are passed through.** If an attempt emits some values and then errors, those values reach the subscriber, and the retry emits them again. - **Add jitter.** Many clients failing at the same moment and retrying on the same schedule arrive together again; a random component spreads them out. - **Place `retry` before any fallback.** A `catchError` above `retry` would swallow the error before `retry` sees it. ## Migrating from retryWhen `retryWhen(errors$ => ...)` received a stream of errors and returned a notifier. It is deprecated in RxJS 7 — its deprecation note says it will be removed in v9 or v10 — in favour of `retry`'s `delay` option, which receives each error and the retry count directly and needs no hand-written counting with `scan` or `zip`.
- Why can retry with a delay function turn a failure into a silent success?When the notifier returned by `delay` completes without emitting, `retry` completes the result instead of retrying or erroring. Returning `EMPTY` to mean 'stop retrying' therefore hides the failure. To give up and surface the error, return `throwError(() => error)` from the delay function.
- What does resetOnSuccess change for a long-lived price ticker?With the default `false`, retries are counted across the stream's whole life, so occasional failures hours apart eventually exhaust `count` and end the stream. With `resetOnSuccess: true`, the counter resets whenever the resubscribed source emits a value, so `count` limits consecutive failures only, and the backoff restarts from its first step.
- How many times is the endpoint called with retry({ count: 3 }) if every attempt fails?Four times: the original subscription plus three retries. After the third retry fails, `count` is exhausted and the last error is passed downstream.
saying these in an interview costs you the question
- retryWhen is still the recommended way to add a delay between retries
- retry({ count: 3 }) sends the request three times in total
- Returning EMPTY from the delay function makes retry rethrow the error
- retryCount passed to the delay function starts at zero
- resetOnSuccess is on by default, so the counter always resets after a success