api-retry-lite
v1.0.0
Published
Lightweight retry wrapper for fetch with exponential backoff, timeout support, and hooks
Maintainers
Readme
api-retry-lite
Lightweight retry wrapper for fetch with exponential backoff, timeout support, and hooks. Zero dependencies, fully typed, and tiny footprint.
- 🎯 Simple API - Drop-in replacement for fetch
- ⚡ Lightweight - ~2-3KB minified, zero dependencies
- 🔄 Smart Retries - Exponential backoff with configurable jitter
- ⏱️ Timeout Support - Built-in request timeout handling
- 🪝 Hooks -
onRetrycallback for logging and metrics - 🎨 Fully Typed - 100% TypeScript with strict mode
- 🔌 Drop-in Ready - Works with any HTTP client that uses fetch
Installation
npm install api-retry-liteQuick Start
import { createRetrier } from 'api-retry-lite';
// Create a retryable fetch function
const retryFetch = createRetrier({
maxRetries: 3,
backoffMs: 100,
timeout: 10000
});
// Use it like regular fetch
try {
const response = await retryFetch('/api/users');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Request failed:', error);
}Features
Exponential Backoff
Automatically retries failed requests with exponentially increasing delays:
const retryFetch = createRetrier({
backoffMs: 100, // Start with 100ms
backoffMultiplier: 2, // Double each time
maxBackoffMs: 30000, // Cap at 30s
jitter: true // Add ±25% randomness
});Delays: 100ms → 200ms → 400ms → 800ms → ... (capped at 30s)
Configurable Retry Conditions
Retry on specific HTTP status codes:
const retryFetch = createRetrier({
retryOn: [408, 429, 500, 502, 503, 504] // Default
});Automatically retries:
- 408 - Request Timeout
- 429 - Too Many Requests (rate limiting)
- 5xx - Server errors (500, 502, 503, 504)
Timeout Support
Set a timeout for each request attempt:
const retryFetch = createRetrier({
timeout: 10000 // 10 seconds per attempt
});When timeout is reached, the request is aborted and retried (unless max retries exhausted).
Abort Signal Support
Respects user abort signals and integrates with timeout:
const controller = new AbortController();
// Request will be aborted if controller.abort() is called
const response = await retryFetch('/api/data', {
signal: controller.signal
});
// Can abort anytime
setTimeout(() => controller.abort(), 5000);Retry Hooks
Monitor retries with onRetry callback for logging, metrics, or custom logic:
const retryFetch = createRetrier({
onRetry: async (info) => {
console.log(`Attempt ${info.attempt} after ${info.delay}ms`, {
status: info.status,
url: info.url,
error: info.error?.message
});
// Can be async for logging to external services
await logToMetrics({
endpoint: info.url,
attempt: info.attempt,
statusCode: info.status
});
}
});RetryInfo object:
attempt- Current retry attempt number (1-indexed)delay- Milliseconds until next retrystatus?- HTTP status code (if available)error?- Error object (if available)url- Request URLresponse?- Response object (if available)
Typed Error Classes
Catch specific error types:
import { RetryError, AbortError, TimeoutError } from 'api-retry-lite';
try {
const response = await retryFetch('/api/data');
} catch (error) {
if (error instanceof RetryError) {
console.error(`Failed after ${error.attempts} attempts`);
console.error('Last response:', error.lastResponse);
console.error('Last error:', error.lastError);
// Get structured details
const details = error.getDetails();
console.error(details);
} else if (error instanceof TimeoutError) {
console.error(`Request timed out after ${error.timeout}ms`);
} else if (error instanceof AbortError) {
console.error('Request was aborted');
}
}Complete Configuration
interface RetryConfig {
// Maximum retry attempts after initial request (default: 3)
maxRetries?: number;
// Initial backoff delay in milliseconds (default: 100)
backoffMs?: number;
// Exponential multiplier for backoff (default: 2)
backoffMultiplier?: number;
// Maximum backoff delay in milliseconds (default: 30000)
maxBackoffMs?: number;
// Add ±25% jitter to backoff delays (default: false)
jitter?: boolean;
// Request timeout in milliseconds (default: 30000)
timeout?: number;
// HTTP status codes that trigger retry (default: [408, 429, 500, 502, 503, 504])
retryOn?: number[];
// Callback before each retry
onRetry?: (info: RetryInfo) => void | Promise<void>;
}Examples
Retry with Logging
const retryFetch = createRetrier({
maxRetries: 5,
backoffMs: 200,
jitter: true,
onRetry: (info) => {
console.log(`Retrying attempt ${info.attempt}/${5 + 1} after ${info.delay}ms`);
if (info.status) {
console.log(` Status: ${info.status}`);
}
if (info.error) {
console.log(` Error: ${info.error.message}`);
}
}
});
const response = await retryFetch('/api/critical-data');With Request Options
const retryFetch = createRetrier();
const response = await retryFetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John' }),
signal: abortController.signal
});Lenient Retry Policy
const retryFetch = createRetrier({
maxRetries: 10,
backoffMs: 50,
backoffMultiplier: 1.5,
maxBackoffMs: 60000,
jitter: true,
timeout: 20000,
retryOn: [408, 429, 500, 502, 503, 504, 522, 524]
});Strict Retry Policy
const retryFetch = createRetrier({
maxRetries: 1,
backoffMs: 100,
timeout: 5000,
retryOn: [429] // Only retry on rate limit
});Metrics Collection
const metrics = {
totalAttempts: 0,
retries: 0,
failures: 0
};
const retryFetch = createRetrier({
onRetry: async (info) => {
metrics.retries++;
metrics.totalAttempts = info.attempt;
// Send to monitoring service
await fetch('/metrics', {
method: 'POST',
body: JSON.stringify({
url: info.url,
attempt: info.attempt,
status: info.status,
error: info.error?.message
})
});
}
});Error Handling
RetryError
Thrown when all retry attempts are exhausted:
try {
await retryFetch('/api/data');
} catch (error) {
if (error instanceof RetryError) {
console.log(error.attempts); // Number of attempts made
console.log(error.lastResponse); // Final response (if available)
console.log(error.lastError); // Last error (if available)
console.log(error.getDetails()); // Structured error info
}
}TimeoutError
Thrown when a request exceeds the configured timeout:
import { TimeoutError } from 'api-retry-lite';
try {
const retryFetch = createRetrier({ timeout: 5000 });
await retryFetch('/api/slow-endpoint');
} catch (error) {
if (error instanceof TimeoutError) {
console.log(error.timeout); // The timeout duration
}
}AbortError
Thrown when a request is aborted by the user:
import { AbortError } from 'api-retry-lite';
try {
const controller = new AbortController();
const retryFetch = createRetrier();
const promise = retryFetch('/api/data', { signal: controller.signal });
controller.abort();
await promise;
} catch (error) {
if (error instanceof AbortError) {
console.log('Request was aborted');
}
}Best Practices
Use jitter for distributed systems - Add jitter to prevent thundering herd:
createRetrier({ jitter: true })Set reasonable timeouts - Prevent hanging requests:
createRetrier({ timeout: 15000 }) // 15 secondsMonitor retries - Log retry attempts for debugging:
createRetrier({ onRetry: (info) => console.log(`Retry ${info.attempt}`) })Be selective about retry codes - Don't retry client errors (4xx):
createRetrier({ retryOn: [408, 429, 500, 502, 503, 504] })Use appropriate backoff - Balance speed with avoiding overload:
createRetrier({ backoffMs: 100, backoffMultiplier: 2, maxBackoffMs: 30000 })
Browser Support
Works in any environment with:
fetchAPI (or polyfill)AbortController(or polyfill)Promisesupport
Performance
- Bundle size: ~2-3KB minified
- Runtime dependencies: None
- No memory leaks: Proper cleanup of timers and event listeners
- Zero allocations: Minimal object creation during retries
License
MIT
Local testing & publishing
Before publishing, verify the package contents locally and test installation from the tarball.
- Create the package tarball:
npm packThis produces a file like api-retry-lite-1.0.0.tgz in the project root.
- Inspect the tarball contents (Linux/macOS/WSL) with:
tar -tzf api-retry-lite-1.0.0.tgzOn Windows PowerShell you can use tar as well if available:
tar -tzf api-retry-lite-1.0.0.tgz- Test-install the tarball in a separate project to verify runtime behavior:
npm install ../path/to/api-retry-lite-1.0.0.tgz
node -e "(async()=>{ const { createRetrier } = require('api-retry-lite'); const retryFetch = createRetrier(); console.log('ok', typeof retryFetch); })()"- When satisfied, publish:
npm login
npm publishIf npm publish fails due to name conflicts, update the package.json name field and bump the version.
