Smart-Retry
Smart utility to retry flaky sync/async operations with delay, backoff, jitter, abort, and retry hooks.

✨ Features
- ✅ Works with sync and async functions
- 🔁 Configurable number of retries and delay
- ⏱️ Exponential backoff and custom schedules
- 🎲 Jitter and randomized backoff strategies
- 🛑 Multiple cancellation options (AbortSignal, predicate, token)
- 🔍
onRetry
hook for logging or metrics
- ⚠️ Optional error filtering with
shouldRetry
- 🧩 Retry until condition or with custom delay logic
- 🪶 Zero dependencies
📦 Install
npm install smart-retry
⚡ Quick Usage
import { retry } from "smart-retry";
async function fetchData() {
const res = await fetch("https://example.com/api");
if (!res.ok) throw new Error("API error");
return res.json();
}
const result = await retry(() => fetchData(), {
retries: 3,
delay: 1000,
factor: 2,
onRetry: (err, attempt) => {
console.warn(`Attempt #${attempt} failed: ${err}`);
},
});
🔄 How Retry Works
The retry mechanism in this library follows a simple but powerful pattern:
- Execution: The library attempts to run your function (sync or async).
- Error Handling:
- If the function succeeds (returns a value without throwing), the result is returned immediately.
- If the function throws an error:
- The library checks if another retry should be attempted based on the
retries
count and shouldRetry
predicate.
- If no more retries are allowed or
shouldRetry
returns false, the error is thrown.
- Notification: If a retry is needed, the
onRetry
callback is invoked with the error and attempt number.
- Delay: The library waits for a specified delay duration before the next attempt.
- For regular retry: The delay increases exponentially by the specified
factor
.
- For jitter: Random variation is added to prevent "thundering herd" problems.
- For scheduled retries: The delay follows a pre-defined array of values.
- Cancellation: Some retry functions support cancellation via:
- AbortSignal: Standard web API for cancellation
- Predicate function: Custom function that returns true when retries should stop
- Cancellation token: Object with a boolean property to cancel retries
All retry functions can handle both synchronous and asynchronous target functions, automatically wrapping results in promises for a consistent async interface.
⚙️ API Overview
Common Options
retries | number | 3 | Number of retry attempts |
delay | number (ms) | 500 | Initial delay between retries |
factor | number | 2 | Multiplier for exponential backoff |
onRetry | (err, attempt) => void | - | Hook called on every failed retry |
shouldRetry | (err) => boolean | always | Predicate to decide if error is retryable |
Provided Functions
retry
: Standard exponential backoff retry.
retryWithJitter
: Adds random jitter to delay.
retryWithTimeout
: Retries until a total timeout is reached.
retryWithSchedule
: Retries using a custom array of delays.
retryWithAbortSignal
: Supports aborting with an AbortSignal
.
retryWithPredicate
: Stops retrying when a predicate function returns true.
retryWithCancellationToken
: Stops retrying when a token object's cancelled
property is true.
retryWithMaxTotalAttempts
: Specify max total attempts (not just retries).
retryWithPredicateDelay
: Custom delay logic per attempt.
retryWithRandomizedBackoff
: Decorrelated jitter/randomized backoff.
retryUntilCondition
: Retries until a user-provided predicate returns true.
retryWithCustomExecution
: Uses custom execution logic instead of try/catch to evaluate success.
🧑💻 Examples
Retry a Synchronous Function
import { retry } from "smart-retry";
let count = 0;
const result = await retry(() => {
count++;
if (count < 3) throw new Error("Oops");
return "done";
});
console.log(result);
Retry Until Condition
import { retryUntilCondition } from "smart-retry";
let value = 0;
await retryUntilCondition(
() => ++value,
(result) => result === 5,
{ retries: 10, delay: 100 },
);
async function checkStatus() {
const res = await fetch("https://api.example.com/status");
const data = await res.json();
return data.status;
}
await retryUntilCondition(
() => checkStatus(),
(status) => status === "ready",
{ retries: 20, delay: 500, onRetry: (err, attempt) => console.log(`Attempt ${attempt}: not ready yet`) },
);
Retry an Async Function with Exponential Backoff and Error Filtering
import { retry } from "smart-retry";
await retry(() => fetchSomething(), {
retries: 5,
delay: 500,
factor: 1.5,
shouldRetry: (err) => err instanceof TimeoutError,
});
Retry with Jitter
import { retryWithJitter } from "smart-retry";
await retryWithJitter(() => fetchSomething(), {
retries: 4,
delay: 200,
jitter: 100,
onRetry: (err, attempt) => {
console.log(`Retry #${attempt} failed: ${err}`);
},
});
Retry with AbortSignal
import { retryWithAbortSignal } from "smart-retry";
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
try {
await retryWithAbortSignal(() => fetchSomething(), {
retries: 10,
delay: 200,
signal: controller.signal,
});
} catch (err) {
if (controller.signal.aborted) {
console.log("Retry aborted by user.");
} else {
console.error("Retry failed:", err);
}
}
Retry with Predicate
import { retryWithPredicate } from "smart-retry";
let stopRetrying = false;
setTimeout(() => {
stopRetrying = true;
}, 1000);
try {
await retryWithPredicate(
() => fetchFromSlowAPI(),
() => stopRetrying,
{ retries: 10, delay: 200 },
);
} catch (err) {
if (stopRetrying) {
console.log("Retry stopped by predicate");
} else {
console.error("Retry failed:", err);
}
}
Retry with Cancellation Token
import { retryWithCancellationToken } from "smart-retry";
const token = { cancelled: false };
setTimeout(() => {
token.cancelled = true;
}, 1000);
try {
await retryWithCancellationToken(() => fetchSomething(), {
retries: 5,
delay: 200,
cancellationToken: token,
});
} catch (err) {
if (token.cancelled) {
console.log("Retry cancelled via token");
} else {
console.error("Retry failed:", err);
}
}
Retry with Custom Schedule
import { retryWithSchedule } from "smart-retry";
await retryWithSchedule(() => fetchSomething(), [100, 200, 400, 800]);
Retry with Predicate Delay
import { retryWithPredicateDelay } from "smart-retry";
await retryWithPredicateDelay(
() => fetchSomething(),
(err, attempt) => attempt * 100,
{ retries: 5 },
);
Retry with Randomized Backoff
import { retryWithRandomizedBackoff } from "smart-retry";
await retryWithRandomizedBackoff(() => fetchSomething(), {
retries: 5,
delay: 100,
maxDelay: 2000,
onRetry: (err, attempt) => {
console.log(`Randomized backoff retry #${attempt}: ${err}`);
},
});
Retry with Max Total Attempts
import { retryWithMaxTotalAttempts } from "smart-retry";
let tries = 0;
await retryWithMaxTotalAttempts(
() => {
tries++;
if (tries < 4) throw new Error("Still failing");
return "success";
},
5,
{ delay: 100 },
);
Retry with Custom Execution
You can also use a custom execution function to handle the retry logic. This is useful when you want to control how the success or failure of the operation is determined.
import { retryWithCustomExecution, ExecutionResult } from "smart-retry";
async function customAPIExecute<T>(fn: () => Promise<Response>): Promise<ExecutionResult<T>> {
try {
const response = await fn();
if (response.status >= 200 && response.status < 300) {
return {
success: true,
value: response as unknown as T,
};
}
return {
success: false,
error: new Error(`API returned status: ${response.status}`),
value: response as unknown as T,
};
} catch (error) {
return { success: false, error: error as Error };
}
}
try {
const result = await retryWithCustomExecution<Response>(() => fetch("https://api.example.com/data"), {
execute: customAPIExecute,
retries: 3,
delay: 200,
onRetry: (err, attempt) => {
console.log(`Custom execution retry #${attempt}: ${err.message}`);
},
});
const data = await result.json();
} catch (err) {
console.error("All retry attempts failed:", err);
}
📄 License
MIT © jaktestowac.pl
Powered by jaktestowac.pl team!
🌐 Check out GitHub profile for more open-source projects and resources.