skip to content

In RxJS 7, how do timeout's each, first and with options behave, and how do you combine timeout with retry for a price request that hangs?

level: seniorimportance: nice to knowfreq 34%

answer

  1. a hang never errors
  2. gap between values, and the first
  3. TimeoutError or a replacement
  4. timeout inside, retry outside
  5. each attempt gets its own timer

basics

~20 s

timeout errors with a TimeoutError when a value is late: each limits every gap, first the wait for the first value, and with switches to a replacement instead; put timeout before retry so each attempt gets its own timer.

solid answer

~40 s

A request that hangs never errors, so `retry` alone never fires. `timeout` turns silence into an error. `timeout(3000)` is shorthand for `timeout({ each: 3000 })`: every gap between values, and the wait for the first one, may last at most 3 s, otherwise the stream errors with a `TimeoutError` whose `info` holds `seen`, `lastValue` and `meta`. `first` sets a separate deadline for the first value (milliseconds or a `Date`); if only `first` is set, later gaps are unlimited. `with` replaces the error: a function receiving that same info and returning an observable to switch to. For a hanging price request, write `pipe(timeout(3000), retry({ count: 2, delay: 1000 }))`: the timeout unsubscribes the hung attempt and errors, and `retry` resubscribes, starting a fresh timer.

code

ts · 10 lines
ts
import { NEVER, TimeoutError, timeout } from 'rxjs';

NEVER.pipe(timeout({ first: 1000, meta: { sku: 'A-100' } })).subscribe({
  error: (err) => {
    if (err instanceof TimeoutError) {
      console.log('timed out', err.info?.meta, 'values seen:', err.info?.seen);
    }
  },
});
// after ~1 s: timed out { sku: 'A-100' } values seen: 0

go deeper

for a junior

Recall that timeout makes a stream error when no value arrives in time, so retry and catchError can react to a hang.

for a middle

Explain each versus first, the TimeoutError and its info, and the with option that switches to a replacement instead of erroring.

for a senior

Order timeout, retry and catchError deliberately, and choose per-attempt versus overall deadlines with limits that do not turn slow successes into failures.

for a principal

Align client timeouts and retry budgets with server-side timeouts so the two layers never retry work the server is still doing.

## Why a hang needs timeout `retry` and `catchError` react to **errors**. A request that never answers produces no error — it simply stays open. Nothing downstream will ever react, and a loading indicator stays on screen indefinitely. `timeout` converts "no value arrived in time" into an error that the rest of the pipeline can handle. ## The options `timeout` accepts a number, a `Date`, or a configuration object: | Option | Meaning | |---|---| | `each` | maximum time in ms between values; also used for the first value when `first` is not given | | `first` | deadline for the first value: milliseconds, or a `Date` | | `with` | function `(info) => ObservableInput` to switch to instead of erroring | | `meta` | any data you want attached to the timeout information | | `scheduler` | the scheduler that runs the timers; `asyncScheduler` by default | The short forms map onto it: - `timeout(3000)` is `timeout({ each: 3000 })`; - `timeout(someDate)` is `timeout({ first: someDate })`. At least one of `first` and `each` is required. With only `first`, once the first value has arrived no further timing is enforced. With only `each`, the same limit applies to the first value and to every gap after it. ## What happens when time runs out 1. `timeout` unsubscribes from the source, which cancels the pending work. 2. Without `with`, the stream errors with a **`TimeoutError`**. Its `info` property carries `seen` (how many values arrived), `lastValue` and your `meta`. 3. With `with`, `timeout` calls it with that same information and subscribes the subscriber to the returned observable instead — no error. Each value that arrives restarts the `each` timer; completion and unsubscription clear it. ## Combining timeout with retry and a fallback ```ts import { Observable, catchError, of, retry, timeout } from 'rxjs'; interface Price { sku: string; amount: number | null } declare function fetchPrice(sku: string): Observable<Price>; const price$ = fetchPrice('A-100').pipe( timeout({ first: 3000, meta: { sku: 'A-100' } }), // each attempt: 3 s retry({ count: 2, delay: 1000 }), // up to 3 attempts catchError(() => of({ sku: 'A-100', amount: null })), ); ``` The order matters: - **`timeout` before `retry`:** each attempt has its own 3-second budget. A hung attempt is unsubscribed and errors; `retry` resubscribes, which starts a new request and a new timer. Worst case here is about three attempts of 3 s plus two 1-second pauses. - **`timeout` after `retry`:** one timer covers the whole retrying sequence. Retries produce no values, so the timer is not reset between them; if the total exceeds the limit, everything is abandoned at once. Use this deliberately as an **overall** deadline, possibly in addition to a per-attempt one. - **`catchError` last:** it sees an error only after every retry has failed or timed out. ## Timeout versus a fallback with `with` `with` can replace `catchError` for the timeout case alone: `timeout({ first: 3000, with: () => of(cachedPrice) })` serves a cached price when the request is slow, while other errors still propagate normally. Because it never errors, `retry` placed below it will not retry a timeout. ## Traps - **No timeout at all** — the most common one: `retry(3)` on a request that hangs never retries. - **`each` on a stream with long natural pauses** — a price ticker that legitimately pauses for a minute errors if `each` is 3 s. Use `first` for "must connect quickly" and a larger `each` for "must not go silent". - **Timeouts that are too short** under load turn slow successes into failures, and the retries add load. - **`timeoutWith`** is deprecated in RxJS 7; its behaviour moved into `timeout`'s `with` option.

  • What is the difference between timeout({ first: 3000 }) and timeout({ each: 3000 })?
    `first` only limits how long the first value may take; after it arrives, no further timing applies. `each` limits every gap between values and, when `first` is not given, the wait for the first value too. For a single-response request they behave the same; for a stream of values they differ sharply.
  • Why does timeout placed after retry not give each attempt its own time limit?
    A timeout below `retry` subscribes once to the retrying stream. Resubscriptions inside `retry` emit no values, so the timer keeps running across attempts and covers the whole sequence. To limit each attempt, put `timeout` above `retry`, so every resubscription creates a fresh timeout.

saying these in an interview costs you the question

  • retry alone recovers from a request that hangs without answering
  • timeout(3000) only limits the wait for the first value
  • When timeout fires it leaves the hung request running in the background
  • timeout placed after retry gives every attempt its own time limit
  • timeoutWith is the recommended way to switch to a fallback on timeout