A 500 or network error (status 0) is often a temporary glitch, worth retrying a second later. A 400 (bad request) or 401 (not authorized) is a broken payload or an expired token — pointless to retry.
The problem it solves
A plain retry(3) retries EVERYTHING, including 400s. We annoy the backend with needless load and mask real bugs in our own code. You need conditional logic: what to retry, what not, with what delay.
Operators and why they matter
retry({ count, delay }) — the delay callback receives the error and decides whether to retry it.
timer(retryIndex * 10) — backoff: the second attempt waits longer than the first.
throwError inside the delay callback — stops the retry for permanent errors. Return an error → the retry turns into a real fail.
catchError — turn the final failure (after all attempts) into a UI state.
Gotchas
retry with no limit (count) — an infinite loop if the error is permanent.
Without a condition you retry 400/401 — pointless and annoying to the backend.
Don't retry a non-idempotent mutation (a POST without an idempotency key) — two charges, two messages, two bookings.
What you get
A transient failure recovers transparently. A client error instantly becomes a clear failure without a series of useless retries.