function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function fixedDelay(intervalMs) {
return function getDelay() {
return intervalMs;
};
}
function exponentialDelay(options = {}) {
const {
baseMs = 500,
factor = 2,
maxMs = Infinity,
} = options;
return function getDelay(attempt) {
const delay = baseMs * Math.pow(factor, attempt - 1);
return Math.min(delay, maxMs);
};
}
async function retryer(fn, options = {}) {
const {
maxAttempts = 3,
delay = 0,
shouldRetry = () => true,
onRetry,
} = options;
if (typeof fn !== "function") {
throw new TypeError("retryer expected fn to be a function");
}
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
throw new TypeError("maxAttempts must be an integer greater than or equal to 1");
}
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await Promise.resolve(fn());
} catch (error) {
lastError = error;
const isLastAttempt = attempt === maxAttempts;
if (isLastAttempt) {
throw lastError;
}
const canRetry = await Promise.resolve(shouldRetry(error, attempt));
if (!canRetry) {
throw lastError;
}
const delayMs =
typeof delay === "function"
? delay(attempt, error)
: delay;
if (onRetry) {
onRetry({
error,
attempt,
maxAttempts,
delayMs,
});
}
if (delayMs > 0) {
await sleep(delayMs);
}
}
}
throw lastError;
}
let count = 0;
async function unstableApiCall() {
count++;
if (count < 3) {
throw new Error("API failed");
}
return "Success!";
}
retryer(unstableApiCall, {
maxAttempts: 5,
delay: fixedDelay(1000),
onRetry: ({ attempt, delayMs, error }) => {
console.log(
`Attempt ${attempt} failed: ${error.message}. Retrying in ${delayMs}ms`
);
},
}).then(console.log);
let attempts = 0;
async function flakyRequest() {
attempts++;
if (attempts < 4) {
throw new Error("Temporary failure");
}
return "Done";
}
retryer(flakyRequest, {
maxAttempts: 5,
delay: exponentialDelay({
baseMs: 500,
factor: 2,
maxMs: 5000,
}),
onRetry: ({ attempt, delayMs }) => {
console.log(`Retry ${attempt}, waiting ${delayMs}ms`);
},
}).then(console.log);
async function runTests() {
{
const result = await retryer(() => "ok", {
maxAttempts: 3,
});
console.assert(result === "ok", "Test 1 failed");
}
{
let attempts = 0;
const result = await retryer(() => {
attempts++;
if (attempts < 3) {
throw new Error("fail");
}
return "success";
}, {
maxAttempts: 5,
});
console.assert(result === "success", "Test 2 result failed");
console.assert(attempts === 3, "Test 2 attempts failed");
}
{
let attempts = 0;
const result = await retryer(async () => {
attempts++;
if (attempts < 2) {
throw new Error("async fail");
}
return "async success";
}, {
maxAttempts: 3,
});
console.assert(result === "async success", "Test 3 result failed");
console.assert(attempts === 2, "Test 3 attempts failed");
}
{
let attempts = 0;
try {
await retryer(() => {
attempts++;
throw new Error("always fails");
}, {
maxAttempts: 3,
});
console.assert(false, "Test 4 should have thrown");
} catch (error) {
console.assert(error.message === "always fails", "Test 4 error failed");
console.assert(attempts === 3, "Test 4 attempts failed");
}
}
{
let attempts = 0;
try {
await retryer(() => {
attempts++;
throw new Error("do not retry");
}, {
maxAttempts: 5,
shouldRetry: () => false,
});
console.assert(false, "Test 5 should have thrown");
} catch (error) {
console.assert(error.message === "do not retry", "Test 5 error failed");
console.assert(attempts === 1, "Test 5 attempts failed");
}
}
{
const delay = fixedDelay(1000);
console.assert(delay(1) === 1000, "Test 6 attempt 1 failed");
console.assert(delay(5) === 1000, "Test 6 attempt 5 failed");
}
{
const delay = exponentialDelay({
baseMs: 100,
factor: 2,
maxMs: 1000,
});
console.assert(delay(1) === 100, "Test 7 attempt 1 failed");
console.assert(delay(2) === 200, "Test 7 attempt 2 failed");
console.assert(delay(3) === 400, "Test 7 attempt 3 failed");
console.assert(delay(5) === 1000, "Test 7 max cap failed");
}
console.log("All tests passed");
}
runTests();