Implement retry() with Exponential Backoff

retry(fn, attempts) re-executes an async function on failure. Build up from a fixed retry loop to exponential backoff, a maxDelay cap, and a shouldRetry predicate to skip unretryable errors.

12 min read
JavaScript
Interview
Implementation
Async

TABLE OF CONTENTS

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:

  1. Attempt counter — try up to N times
  2. On success — return immediately
  3. 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 structurefor with try/catch inside, 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 formuladelay * factor^i — naming it "exponential backoff" and explaining why it helps
  • maxDelay cap — preventing infinite wait times at high attempt counts
  • No delay on last attempt — skip the wait if you're about to throw anyway

Complexity

TimeSpace
Best case (1st succeeds)O(T) of fnO(1)
Worst case (all fail)O(N × T + total delay)O(1)

Interview Tips

  • Start with the loop, not recursion — a for loop 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; call throw err immediately 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)."

Related Questions


Let's Connect

© 2026 Naveen Karthik // Built with React & MUI