call(back)
Algorithms & Data StructuresmediumSign in to star problems

Retry Wrapper

Implement a retry wrapper. The loop is short; the grading is in the details — where exceptions are caught, how attempts are counted, and where the sleep goes.

const retryer = new Retryer({ maxAttempts: 3, backoff: backoff.exponentialJitter(200), isRetryable: (err) => err.status >= 500 });
await retryer.run((attempt) => fetchJson(url));       // resolves with fn's value, or rethrows
const safeFetch = retryer.wrap(fetchJson);              // decorator form

Contract

  • backoff strategies map an attempt number (0-based) to a delay in ms: fixed(ms), linear(ms) (ms × (attempt + 1)), exponential(base, cap) (min(cap, base × 2^attempt)), and exponentialJitter(base, cap, random) (the exponential delay × [0.5, 1.5), using the injected random).
  • new Retryer({ maxAttempts = 3, backoff, isRetryable = () => true, onAttempt, onFailure, sleep }). sleep(ms) defaults to a real timer; the grader injects one that only records the delay.
  • run(fn, { signal }): call fn(attempt) up to maxAttempts times. Before each attempt call signal?.throwIfAborted?.(), then onAttempt(attempt). On failure call onFailure(err, attempt); rethrow immediately if the error isn't retryable or this was the last attempt; otherwise await sleep(backoff(attempt)) and try again. Resolve with fn's value.
  • wrap(fn) returns (...args) => run(() => fn(...args)).

Hints

Hint 1

Loop over attempts; put the await fn(attempt) inside the try so async rejections are caught, and rethrow when the error fails isRetryable or you're on the last attempt.

Hint 2

Sleep only between attempts — after a failure that will be retried — never after the last one. The injected sleep makes that ordering observable.

Hint 3

Keep the strategies as tiny factories returning (attempt) => ms; jitter multiplies by 0.5 + random() so a fleet of clients doesn't retry in lockstep.

Asked at