Async functions fail. Networks drop, servers return 500s, rate limits kick in. SmartRetry wraps any async function with configurable retry logic — exponential backoff, jitter, global timeouts, and cancellation via AbortSignal — so you don't have to write that boilerplate again.
- Exponential backoff with configurable base and max delay
- Full jitter to prevent thundering herd problems
- Global timeout across all attempts (not per-attempt)
- AbortSignal support for cancellation
- Intelligent default policy — retries network errors, 429s, and 5xx; stops on 4xx
- Custom retry predicates — full control over what gets retried
- Zero runtime dependencies
- ESM + CJS dual build with proper type declarations
- Tree-shakeable and side-effect free
- Node.js >= 18 and modern browsers
Most retry implementations:
- Retry too aggressively
- Do not support global timeout
- Lack AbortSignal support
- Wrap non-retryable errors incorrectly
SmartRetry focuses on correctness, predictable behavior, and production-safe defaults.
npm install @rootvector/smart-retryimport { smartRetry } from "@rootvector/smart-retry";
const data = await smartRetry(
async (attempt) => {
const res = await fetch("https://api.example.com/data");
if (!res.ok) {
const err: any = new Error(`HTTP ${res.status}`);
err.status = res.status;
throw err;
}
return res.json();
},
{
maxRetries: 5,
baseDelayMs: 200,
timeoutMs: 10000,
}
);The attempt parameter starts at 0 for the initial call. maxRetries controls additional attempts after the first, so total attempts = 1 + maxRetries.
import { smartRetry } from "@rootvector/smart-retry";
await smartRetry(callExternalService, {
maxRetries: 4,
retryOn: (error, attempt) => {
// Only retry on specific conditions
if (error instanceof TypeError) return true;
if (error instanceof Error && error.message.includes("rate limit")) return true;
return false;
},
onRetry: (error, attempt, delay) => {
console.log(`Attempt ${attempt} in ${Math.round(delay)}ms...`);
},
});import { smartRetry, AbortError } from "@rootvector/smart-retry";
const controller = new AbortController();
setTimeout(() => controller.abort(), 3000);
try {
await smartRetry(fn, {
maxRetries: 10,
signal: controller.signal,
});
} catch (err) {
if (err instanceof AbortError) {
// Operation was cancelled
}
}import { smartRetry, TimeoutError } from "@rootvector/smart-retry";
try {
await smartRetry(fn, {
maxRetries: 10,
timeoutMs: 15000, // 15 seconds total, not per-attempt
});
} catch (err) {
if (err instanceof TimeoutError) {
console.error(`Timed out after ${err.totalElapsedMs}ms`);
}
}Executes fn and retries on failure according to the provided options.
| Parameter | Type | Description |
|---|---|---|
fn |
(attempt: number) => Promise<T> |
Async function to execute. attempt is 0-indexed. |
options |
RetryOptions |
Optional configuration. |
| Option | Type | Default | Description |
|---|---|---|---|
maxRetries |
number |
3 |
Retry attempts after the initial call. Total = 1 + maxRetries. |
baseDelayMs |
number |
300 |
Base delay (ms) for exponential backoff. |
maxDelayMs |
number |
5000 |
Upper bound for computed delay. |
jitter |
boolean |
true |
Apply full jitter: random(0, computedDelay). |
retryOn |
(error, attempt) => boolean |
default policy | Return true to retry, false to stop. |
onRetry |
(error, attempt, delay) => void |
— | Called before each retry. |
timeoutMs |
number |
— | Global timeout across all attempts. |
signal |
AbortSignal |
— | Cancellation signal. |
Standalone utility that returns true if the error carries an HTTP status of 429 or 500–599. Inspects error.status and error.response?.status.
delay = min(maxDelayMs, baseDelayMs * 2^retryIndex)
Where retryIndex is 0 for the first retry. With jitter enabled, the final delay is random(0, delay).
When no retryOn predicate is provided, SmartRetry uses a built-in policy:
| Condition | Retried? |
|---|---|
Network errors (ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN) |
Yes |
| HTTP 429 (Too Many Requests) | Yes |
| HTTP 500–599 (Server errors) | Yes |
| HTTP 400–499 (Client errors, except 429) | No |
| Errors without a recognized status or code | Yes |
All errors thrown by SmartRetry extend SmartRetryError and include:
totalAttempts— number of attempts madetotalElapsedMs— total wall-clock time in millisecondscause— the original error (via standardErrorOptions)
| Error Class | When Thrown |
|---|---|
RetryExhaustedError |
All retry attempts failed |
TimeoutError |
Global timeoutMs exceeded |
AbortError |
AbortSignal was aborted |
When retryOn returns false, the original error is rethrown directly — it is not wrapped in RetryExhaustedError.
import { smartRetry, RetryExhaustedError, TimeoutError } from "@rootvector/smart-retry";
try {
await smartRetry(fn, { maxRetries: 3, timeoutMs: 5000 });
} catch (err) {
if (err instanceof TimeoutError) {
console.error(`Timed out after ${err.totalElapsedMs}ms`);
} else if (err instanceof RetryExhaustedError) {
console.error(`Failed after ${err.totalAttempts} attempts:`, err.cause);
}
}Wrapping an API call with structured retry logic:
import { smartRetry, TimeoutError, RetryExhaustedError } from "@rootvector/smart-retry";
async function fetchUser(userId: string) {
return smartRetry(
async () => {
const res = await fetch(`https://api.example.com/users/${userId}`);
if (!res.ok) {
const err: any = new Error(`HTTP ${res.status}`);
err.status = res.status;
throw err;
}
return res.json();
},
{
maxRetries: 3,
baseDelayMs: 500,
maxDelayMs: 5000,
timeoutMs: 15000,
onRetry: (error, attempt, delay) => {
console.warn(`Retry ${attempt} for user ${userId} in ${Math.round(delay)}ms`);
},
}
);
}Invalid options throw synchronously with descriptive messages:
maxRetries < 0baseDelayMs <= 0maxDelayMs < baseDelayMstimeoutMs <= 0
SmartRetry follows Semantic Versioning (SemVer).
- Patch — Bug fixes
- Minor — Backward-compatible improvements
- Major — Breaking API changes
Contributions are welcome. Please open an issue to discuss proposed changes before submitting a pull request.
git clone https://github.com/rootvector2/SmartRetry.git
cd SmartRetry
npm install
npm test
npm run buildMIT
SmartRetry is maintained as open-source infrastructure software.
If it provides value in your projects, consider supporting its long-term maintenance:
👉 https://opencollective.com/SmartRetry
Contributions help fund:
- Ongoing maintenance and bug fixes
- Test infrastructure and CI
- Documentation improvements
- Performance validation and benchmarking
All development remains public and transparent.