smart-retry-js
v1.0.0
Published
Production-ready retry utility with exponential back-off, jitter, per-attempt timeouts, and a circuit breaker. Zero runtime dependencies.
Maintainers
Readme
smart-retry-js
Production-ready retry utility for TypeScript & Node.js — exponential back-off, jitter, per-attempt timeouts, and a circuit breaker. Zero runtime dependencies.
Features
| Feature | Description |
|---|---|
| Exponential Back-off | Delay doubles on each retry attempt |
| Jitter | full, equal, or none — prevents thundering-herd problems |
| Circuit Breaker | Stops calling a broken service, auto-recovers after a cooldown |
| Per-attempt Timeout | Rejects with TimeoutError if a single attempt is too slow |
| onRetry hook | Called after each failed attempt with the error and attempt index |
| shouldRetry predicate | Veto retries based on the error type (e.g. never retry 4xx) |
| Dual ESM + CJS | Works in both import and require environments |
| Full type exports | All types exported; works with strict TypeScript |
Installation
npm install smart-retry-jsQuick Start
import { retry } from 'smart-retry-js';
const data = await retry(
() => fetch('https://api.example.com/data').then(r => r.json()),
{ retries: 5, delay: 200 },
);API
retry(asyncFn, options?)
| Parameter | Type | Description |
|---|---|---|
| asyncFn | () => Promise<T> | The async operation to execute |
| options | RetryOptions | Configuration (all fields optional) |
Returns Promise<T> — resolves with the function's return value, or rejects with the last error after all retries are exhausted.
Options Reference
interface RetryOptions {
retries?: number; // max attempts (default: 3)
delay?: number; // base delay in ms (default: 100)
backoff?: number; // delay multiplier (default: 2)
jitter?: 'full' | 'equal' | 'none'; // (default: 'none')
timeout?: number; // per-attempt timeout ms (no timeout by default)
circuitBreaker?: {
threshold: number; // failures before opening circuit
cooldown: number; // ms before half-open retry
};
onRetry?: (error: unknown, attempt: number) => void;
shouldRetry?: (error: unknown) => boolean;
}Examples
1. Basic Retry
import { retry } from 'smart-retry-js';
const result = await retry(
() => unstableApiCall(),
{ retries: 3 },
);2. Exponential Back-off
Each retry waits delay × backoff^attempt milliseconds.
const result = await retry(
() => fetchData(),
{
retries: 5,
delay: 100, // 100ms → 200ms → 400ms → 800ms → 1600ms
backoff: 2,
jitter: 'none',
},
);3. Jitter
Full Jitter
Delay is a random value in [0, exponentialDelay] — maximum variance, best for high-throughput scenarios.
await retry(() => callApi(), {
retries: 5,
delay: 100,
backoff: 2,
jitter: 'full',
});Equal Jitter
Delay is exponentialDelay/2 + random(0, exponentialDelay/2) — balanced variance, guaranteed minimum wait.
await retry(() => callApi(), {
retries: 5,
delay: 100,
backoff: 2,
jitter: 'equal',
});4. Per-attempt Timeout
Each attempt is individually time-boxed. Slow attempts throw TimeoutError.
import { retry, TimeoutError } from 'smart-retry-js';
try {
await retry(() => slowDatabaseQuery(), {
retries: 3,
timeout: 2_000, // each attempt must finish in 2s
});
} catch (err) {
if (err instanceof TimeoutError) {
console.error(`Attempt ${err.attempt} timed out`);
}
}5. Circuit Breaker
The circuit breaker tracks failures across calls. Once the threshold is reached the circuit opens and further calls are rejected immediately with CircuitOpenError — without even calling your function. After the cooldown the circuit enters HALF_OPEN and allows one trial call through.
import { retry, CircuitOpenError } from 'smart-retry-js';
try {
await retry(() => callDownstreamService(), {
retries: 10,
delay: 50,
circuitBreaker: {
threshold: 5, // open after 5 consecutive failures
cooldown: 10_000, // try again after 10 seconds
},
});
} catch (err) {
if (err instanceof CircuitOpenError) {
console.warn('Circuit is open — service is unavailable');
}
}6. onRetry Hook
Useful for logging, metrics, or alerting on each failed attempt.
await retry(() => fetchData(), {
retries: 5,
onRetry(error, attempt) {
console.warn(`Attempt ${attempt} failed:`, error);
metrics.increment('api.retry', { attempt });
},
});7. shouldRetry Predicate
Control which errors are worth retrying. Return false to abort immediately.
class NotFoundError extends Error {}
class RateLimitError extends Error {}
await retry(() => callApi(), {
retries: 5,
shouldRetry(error) {
// Never retry client errors
if (error instanceof NotFoundError) return false;
// Always retry transient errors
if (error instanceof RateLimitError) return true;
return true;
},
});8. Using the Circuit Breaker Standalone
You can use CircuitBreaker independently of retry():
import { CircuitBreaker } from 'smart-retry-js';
const cb = new CircuitBreaker({ threshold: 3, cooldown: 5_000 });
// Execute through the circuit breaker
const result = await cb.execute(() => callExternalApi());
// Inspect current state
console.log(cb.getSnapshot());
// { state: 'CLOSED', failures: 0, lastFailureTime: null }Error Types
import { TimeoutError, CircuitOpenError } from 'smart-retry-js';| Class | When thrown |
|---|---|
| TimeoutError | Per-attempt timeout expired. Has .attempt (1-based number) |
| CircuitOpenError | Circuit breaker is in OPEN state — call blocked entirely |
State Machine
failure × threshold
CLOSED ─────────────────────► OPEN
▲ │
│ success │ cooldown elapsed
│ ▼
└───────────────────── HALF_OPEN
success / failureTypeScript
All types are exported. No any usage, strict mode throughout.
import type {
RetryOptions,
CircuitBreakerOptions,
CircuitBreakerSnapshot,
CircuitState,
JitterStrategy,
} from 'smart-retry-js';Development
# Install dependencies
npm install
# Build (ESM + CJS + .d.ts)
npm run build
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Coverage report
npm run test:coverage
# Type-check without emitting
npm run typecheckLicense
MIT © Gaurav Kathiriya
