skip to content

In RxJS, what does retry(3) do when a request observable errors, and how is that different from catchError?

level: juniorimportance: should knowfreq 58%

answer

  1. resubscribe, do not replace
  2. the request is sent again
  3. three retries, four attempts
  4. order: retry, then fallback

basics

~10 s

retry(3) catches an error by resubscribing to the source, which re-runs a cold request up to three more times, then passes the last error on; catchError instead replaces the failed stream with another observable.

solid answer

~40 s

`retry(3)` intercepts an `error` and **resubscribes** to its source. For a cold request observable, subscribing sends the request, so each retry is a new request: at most **four attempts** — the original plus three retries — after which the last error goes downstream. `retry()` with no argument retries forever; `retry(0)` does not retry. `catchError` works differently: it **replaces** the rest of the stream with the observable its selector returns, such as a fallback value. They combine: `retry(3)` first, then `catchError(() => of(fallback))`, so the fallback is used only after every retry has failed. Reversed, `catchError` swallows the error before `retry` ever sees one.

code

ts · 17 lines
ts
import { Observable, retry } from 'rxjs';

let attempt = 0;
const source$ = new Observable<number>((subscriber) => {
  attempt++;
  subscriber.next(1);
  subscriber.next(2);
  if (attempt < 2) {
    subscriber.error(new Error('flaky'));
  } else {
    subscriber.next(3);
    subscriber.complete();
  }
});

source$.pipe(retry(1)).subscribe((v) => console.log(v));
// 1 2 1 2 3

go deeper

for a junior

Recall that retry resubscribes to a failing source a given number of times, while catchError switches to a replacement observable.

for a middle

Explain retries versus attempts, retry() and retry(0), values from failed attempts being forwarded, and why retry must sit above catchError.

for a senior

Decide which failures deserve a retry at all, keep side-effecting requests out of blind retries, and never ship unlimited immediate retries.

for a principal

Set a team-wide rule for retries on requests: bounded counts, delays, and which error classes are retryable, so clients do not amplify an outage.

## Two ways to react to an error In RxJS an `error` notification ends a stream. Two operators let a pipeline react before the error reaches the subscriber: - **`retry`** tries the **same** source again by subscribing to it once more. - **`catchError`** gives up on the source and **switches** to a replacement observable. Both only see errors that come from above them in the `pipe`. ## What retry(n) does 1. The subscriber subscribes; `retry` subscribes to its source. 2. If the source errors, `retry` checks its counter. While it has retries left, it unsubscribes from the failed attempt and **subscribes to the source again**. 3. If the new attempt succeeds, its values and completion flow to the subscriber as normal. 4. When the retries are used up, the **last** error is passed downstream. Because `retry` resubscribes, what repeats is whatever **subscribing** does. For a cold request observable — one that sends its request when subscribed, which is how request observables are normally built — each retry sends a new request. For a hot source such as a `Subject`, resubscribing re-runs nothing — and a `Subject` that has already errored simply delivers the same error again at once. ## Counting | Call | Attempts in total | Notes | |---|---|---| | `retry()` | unlimited | retries forever on a persistent error | | `retry(0)` | 1 | no retries at all | | `retry(3)` | up to 4 | the original plus three retries | | `retry({ count: 3, delay: 1000 })` | up to 4 | waits one second before each retry | A common slip in interviews is saying `retry(3)` sends the request three times; the count is of **retries**, not attempts. ## Values from failed attempts `retry` forwards every value an attempt emits, even from an attempt that later fails. If a source emits `1, 2` and then errors, and the retry emits `1, 2, 3` and completes, the subscriber sees `1, 2, 1, 2, 3`. A single-response request emits nothing before it fails, so this rarely matters there, but it does for streams that emit several values. ## retry versus catchError | | `retry` | `catchError` | |---|---|---| | Reaction | resubscribe to the same source | subscribe to a replacement | | Source work | runs again | abandoned | | Result after success | the source's own values | the replacement's values | | Typical use | transient failures | a default value, a domain error, or giving up quietly | ## Combining them in the right order ```ts import { Observable, catchError, of, retry } from 'rxjs'; interface Price { sku: string; amount: number | null } declare function fetchPrice(sku: string): Observable<Price>; const price$ = fetchPrice('A-100').pipe( retry(3), // up to 4 attempts catchError(() => of({ sku: 'A-100', amount: null })), // only after all fail ); ``` - **`retry` above `catchError`:** transient failures are retried; only when every retry has failed does the fallback apply. - **`catchError` above `retry`:** the first error is turned into the fallback, the stream completes normally, and `retry` never sees an error — no retries ever happen. ## When not to retry - **Errors that will not change** — a 404, a validation error, a permission error — fail identically on every retry and only add load and delay. Filter them with the config form of `retry`, whose `delay` function can refuse to retry. - **Requests with side effects**, such as placing an order, can be duplicated if the first attempt actually succeeded on the server before the response was lost. - **Unlimited `retry()`** against a persistent failure keeps sending requests for as long as someone is subscribed.

  • Why does retry(3) placed after catchError never retry?
    `catchError` intercepts the first error and replaces the stream with its fallback, which completes normally. From `retry`'s position there was no error at all, so it has nothing to retry. Put `retry` above `catchError` so retries happen first and the fallback applies only when they are exhausted.
  • What does retry() with no argument do against an endpoint that is down?
    It resubscribes immediately after every error, without limit and without waiting, so a cold request observable sends a new request each time for as long as someone is subscribed. Always give a count, and usually a delay, for requests.

retry is redialling the same number after the call drops; catchError is hanging up and calling a different number instead.

saying these in an interview costs you the question

  • retry(3) sends the request exactly three times in total
  • retry replaces the failed stream with a fallback value
  • Placing catchError before retry still lets retry re-run the request
  • retry() with no argument does not retry at all
  • retry drops the values an attempt emitted before it failed