Retry wraps an async function and re-executes it on failure, up to N times. The base version is a loop — but the interview escalates to exponential backoff, selective retry based on error type, and combining with a timeout deadline.
What is retry()?
retry(fn, attempts) calls fn() and, if it rejects, calls it again — up to attempts times total. On the final attempt, if it still fails, the last error propagates.
This is the resilience pattern. Where promiseTimeout says "give up after N ms", retry says "try again up to N times." In production they're usually combined: retry with a timeout per attempt.
Real-world use cases:
- Network requests — transient failures (502, network blip) should be retried; client errors (404, 403) should not
- Database connections — retry on connection refused during startup
- Third-party APIs — rate-limited responses (429) warrant a retry after a delay
- File system operations — retry on temporary lock contention
The interview escalates: fixed delay → exponential backoff → shouldRetry predicate to skip retrying on certain errors → combining with promiseTimeout for a per-attempt deadline.
The Problem
"Implement retry(fn, attempts) that calls fn() up to attempts times. If all attempts fail, reject with the last error."
The interviewer extends:
"Add a delay between retries. Then make it exponential backoff. Then add a shouldRetry option so we can skip retry for certain errors."
Thought Process
A retry loop has three pieces:
- Attempt counter — try up to N times
- On success — return immediately
- On failure — if we have attempts left, wait (optionally) and try again; otherwise throw
A for loop with try/catch inside is the clearest structure. Avoid while(true) — the bounded for makes the attempt count explicit.
Step 1 — Base: Fixed Attempts
Loading editor...
Step 2 — Fixed Delay Between Retries
Loading editor...
Step 3 — Exponential Backoff
Backoff doubles the wait after each failure: 100ms, 200ms, 400ms... This avoids hammering a service that's struggling.
Loading editor...
Step 4 — shouldRetry Predicate
Not all errors should trigger a retry. A 404 should fail immediately. A 503 should retry.
Loading editor...
Full Solution
Loading editor...
What Interviewers Are Testing
- Loop structure —
forwithtry/catchinside, not a recursive approach (stack overflow risk at high attempt counts) - Immediate throw on
shouldRetry= false — not just on last attempt; give up early when the error is unretryable - Backoff formula —
delay * factor^i— naming it "exponential backoff" and explaining why it helps maxDelaycap — preventing infinite wait times at high attempt counts- No delay on last attempt — skip the wait if you're about to throw anyway
Complexity
| Time | Space | |
|---|---|---|
| Best case (1st succeeds) | O(T) of fn | O(1) |
| Worst case (all fail) | O(N × T + total delay) | O(1) |
Interview Tips
- Start with the loop, not recursion — a
forloop is O(1) stack space. Recursion works but mention the trade-off. - Throw early in
shouldRetry: false— don't wait for the loop to exhaust; callthrow errimmediately when the error is unretryable. - Mention
jitter— "In production I'd add random jitter to the delay to prevent thundering herd:wait * (0.5 + Math.random() * 0.5)." This shows real-world awareness. - Combine with
promiseTimeout— "For a per-attempt deadline:retry(() => promiseTimeout(fetch(url), 3000), 3)."