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 formContract
backoffstrategies 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)), andexponentialJitter(base, cap, random)(the exponential delay × [0.5, 1.5), using the injectedrandom).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 }): callfn(attempt)up tomaxAttemptstimes. Before each attempt callsignal?.throwIfAborted?.(), thenonAttempt(attempt). On failure callonFailure(err, attempt); rethrow immediately if the error isn't retryable or this was the last attempt; otherwiseawait sleep(backoff(attempt))and try again. Resolve withfn'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.