Retry a failed call that's likely transient, waiting longer between each attempt so you don't hammer a struggling service.
A blip in network connectivity or a momentary rate limit on the other end isn't a permanent failure — surfacing it to the user immediately, on the first failed attempt, throws away requests that would have succeeded a second later. But retrying instantly, in a tight loop, just adds load right when the dependency needs less of it.
Wrap the call in a loop with a maximum attempt count. After each failure, wait before retrying — typically doubling the wait time (exponential backoff), often with a small random jitter so many clients don't retry in lockstep.
Each failed attempt roughly doubles the wait, plus a little randomness, before the next try — capped at a maximum number of attempts.
async function retryWithBackoff(fn, { maxAttempts = 4, baseDelayMs = 300 } = {}) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxAttempts) throw err;
const backoff = baseDelayMs * 2 ** (attempt - 1);
const jitter = Math.random() * baseDelayMs;
const delay = backoff + jitter;
console.log(`attempt ${attempt} failed, retrying in ${Math.round(delay)}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
let calls = 0;
const flakyFetch = async () => {
calls++;
if (calls < 3) throw new Error("503 service unavailable");
return "ok";
};
const result = await retryWithBackoff(flakyFetch);
console.log(result); // "ok" — succeeded on the 3rd attempt