Retry a Promise-Returning Function with Delay

intermediate
By FrontendAtlas Editorial · Updated Oct 15, 2026
Implement retryWithDelay(fn, retries, delayMs): call a promise-returning function, and when it fails wait delayMs and try again, up to retries extra attempts. Resolve with the first successful value and reject with the last error once every attempt has failed. Public reports of a Google frontend loop and an Intuit senior frontend loop both describe implementing a retry function with a delay between attempts; this practice version pins the contract down so the tests can check attempt counts and timing. Concepts: promises, async, timers, error-handling.

What you'll practice:

  • Settling one outer promise from a chain of attempts without resolving or rejecting it twice
  • Counting attempts so that retries means extra attempts, not total attempts
  • Placing the delay between attempts only, never before the first or after the last

Expected behavior

Your retryWithDelay() implementation should:

  • Return a promise and call fn() immediately for the first attempt
  • Resolve with the first value fn resolves with and never call fn again after that
  • After a failed attempt, wait delayMs and call fn again while attempts remain
  • Make at most retries + 1 attempts, so retries = 0 means a single attempt
  • Reject with the error from the last failed attempt
  • Treat a synchronous throw inside fn like a rejected promise

Arguments

  • fn: () => Promise<T> | T — The operation to run. It may resolve, reject or throw synchronously.
  • retries: number — How many extra attempts to make after the first one fails. Defaults to 3.
  • delayMs: number — Milliseconds to wait between attempts. Defaults to 100.

Returns

Promise<T> — Resolves with the first successful value or rejects with the last error.

Solution idea

Interview answer: define the contract before writing code. One attempt happens immediately; each failure is followed by a delay and another attempt while attempts remain; the total number of attempts is retries + 1; the first success resolves the outer promise and the last failure rejects it. Starting each attempt inside Promise.resolve().then(fn) turns a synchronous throw into a rejection, so a single catch handles both failure kinds. Because a promise settles only once, resolving on the first success automatically ignores any later work, and counting attempts left makes the exit test a single comparison.

Examples
// Succeeds on the third attempt after two failures
let calls = 0;
const flaky = () => (++calls < 3 ? Promise.reject(new Error('busy')) : Promise.resolve('ok'));
await retryWithDelay(flaky, 3, 50); // => 'ok' after about 100 ms, calls === 3

// Gives up after retries + 1 attempts and surfaces the last error
const down = () => Promise.reject(new Error('down'));
await retryWithDelay(down, 2, 10); // rejects with Error('down') after 3 attempts

// retries = 0 is a single attempt
await retryWithDelay(down, 0, 10); // rejects immediately after 1 attempt

Public interview reports

These public, self-reported interview accounts informed this practice version. The exercise is modelled on reported rounds, not copied from them, and the reports are not verified official questions or endorsements.