retryWithBackoff<T>() with capped jitter
TypeScript
export interface RetryOptions {
attempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
jitterRatio?: number;
signal?: AbortSignal;
}
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
/** Run an async task, retrying with capped exponential backoff plus jitter. */
export async function retryWithBackoff<T>(
task: (attempt: number) => Promise<T>,
options: RetryOptions = {},
): Promise<T> {
const { attempts = 4, baseDelayMs = 100, maxDelayMs = 10_000, jitterRatio = 0.2, signal } = options;
let lastError: unknown;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
if (signal?.aborted) throw new Error('retry aborted');
try {
return await task(attempt);
} catch (error) {
lastError = error;
if (attempt === attempts) break;
const capped = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
const jitter = capped * jitterRatio * (Math.random() * 2 - 1);
await sleep(Math.max(0, Math.round(capped + jitter)));
}
}
throw lastError ?? new Error('retry failed without an attempt');
}