Retry
Resilient retry strategies with fluent Schedule API
The Retry system provides resilient retry strategies for operations that might fail temporarily. It uses a fluent Schedule API inspired by Effect-TS, making it easy to compose retry strategies.
Why Use Retry?
Network operations, database connections, and external API calls often fail temporarily. Instead of failing immediately, retry the operation with a smart strategy:
- Exponential backoff - Gradually increase delay between retries
- Jitter - Add randomness to avoid thundering herd
- Composable - Combine multiple strategies
- Type-safe - Full TypeScript support
Basic Usage
Simple Retry
For 95% of cases, just pass a number:
import { retry } from '@deessejs/functions';
// Retry up to 3 times
const fetchWithRetry = retry(fetchUsers, 3);
// Use it
const users = await fetchWithRetry({ limit: 10 });Using Presets
Common retry patterns are built-in:
import { retry, Schedule } from '@deessejs/functions';
// Network failures: exponential backoff + jitter + 5 attempts
const fetchWithRetry = retry(fetchUsers, Schedule.network());
// Quick retry: 3 immediate attempts
const quickRetry = retry(fetchData, Schedule.quick());
// Standard retry: 5 attempts with exponential backoff
const standardRetry = retry(fetchData, Schedule.standard());
// Safe retry: 5 spaced attempts (for idempotent operations)
const safeRetry = retry(fetchData, Schedule.safe());Schedule API
Creating Schedules
Schedules are immutable values that can be composed:
import { Schedule } from '@deessejs/functions';
// Exponential backoff starting at 1000ms
const schedule1 = Schedule.exponential(1000);
// Add jitter (50% randomness by default)
const schedule2 = schedule1.jitter();
// Limit to 5 attempts
const schedule3 = schedule2.attempts(5);
// Combine in one line
const schedule = Schedule.exponential(1000).jitter().attempts(5);Schedule Methods
Factory Methods
Create base schedules:
import { Schedule } from '@deessejs/functions';
// Immediate retry (no delay)
Schedule.immediate()
// Exponential backoff (base delay in ms)
Schedule.exponential(1000) // 1000ms, 2000ms, 4000ms, 8000ms...
// Fixed delay between attempts
Schedule.spaced(2000) // 2000ms, 2000ms, 2000ms...
// Fixed delay (alias for spaced)
Schedule.fixed(2000)
// Fibonacci sequence
Schedule.fibonacci(1000) // 1000ms, 1000ms, 2000ms, 3000ms, 5000ms...Modifiers
Transform existing schedules:
const schedule = Schedule.exponential(1000)
.jitter(0.5) // Add 50% randomness
.attempts(5) // Limit to 5 attempts
.maxDelay(30000) // Maximum 30s between retries
.minDelay(1000); // Minimum 1s between retriesComposition
Combine schedules:
// Use shorter delay
const schedule1 = Schedule.exponential(1000).or(Schedule.spaced(5000));
// Both constraints must be met
const schedule2 = Schedule.exponential(1000).and(Schedule.attempts(5));
// Sequence: 3 immediate, then spaced
const schedule3 = Schedule.attempts(3).immediate().then(Schedule.spaced(2000));Conditions
Stop based on dynamic conditions:
// Stop when predicate returns true
const schedule1 = Schedule.forever().until((attempt, error) => {
return attempt >= 5 || error.code === 'SUCCESS';
});
// Continue while predicate returns true
const schedule2 = Schedule.exponential(1000).while((attempt) => {
return attempt < 10;
});Side Effects
Add logging and monitoring:
const schedule = Schedule.exponential(1000)
.attempts(5)
.tap((attempt, delay) => {
console.log(`Retry attempt ${attempt} after ${delay}ms`);
})
.log(); // Built-in loggingExamples
Network Request with Exponential Backoff
import { retry, Schedule } from '@deessejs/functions';
async function fetchUsers(): Promise<User[]> {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// Retry with exponential backoff + jitter, up to 5 attempts
const fetchWithRetry = retry(fetchUsers, Schedule.exponential(1000).jitter().attempts(5));
const users = await fetchWithRetry();Database Connection with Preset
import { retry, Schedule } from '@deessejs/functions';
async function connectToDatabase(): Promise<Connection> {
return await db.connect({
host: 'localhost',
port: 5432,
});
}
// Use network preset (exponential + jitter + 5 attempts)
const connectWithRetry = retry(connectToDatabase, Schedule.network());
const connection = await connectWithRetry();Fast Then Slow Strategy
import { Schedule } from '@deessejs/functions';
// 3 immediate retries, then switch to spaced retries
const schedule = Schedule.immediate().attempts(3).then(Schedule.spaced(2000));
const fetchWithStrategy = retry(fetchData, schedule);Retry with Logging
import { retry, Schedule } from '@deessejs/functions';
const schedule = Schedule.exponential(1000)
.attempts(5)
.log(); // Built-in logging
const fetchWithLogging = retry(fetchUsers, schedule);
// Console output:
// [Schedule] Attempt 1 after 1000ms
// [Schedule] Attempt 2 after 2000ms
// [Schedule] Attempt 3 after 4000msCustom Logging
import { retry, Schedule } from '@deessejs/functions';
const schedule = Schedule.exponential(1000)
.attempts(5)
.tap((attempt, delay) => {
metrics.record('retry_attempt', { attempt, delay });
logger.info(`Retrying: attempt ${attempt}, delay ${delay}ms`);
});
const fetchWithMetrics = retry(fetchUsers, schedule);Stop on Specific Error
import { retry, Schedule } from '@deessejs/functions';
const schedule = Schedule.exponential(1000)
.until((attempt, error) => {
// Stop if error is not retryable
if (error.code === 'AUTH_INVALID') {
return true; // Stop retrying
}
return attempt >= 5;
});
const fetchSmart = retry(fetchUsers, schedule);Schedule Presets
Built-in presets for common scenarios:
import { Schedule } from '@deessejs/functions';
// Network retry (most common)
Schedule.network()
// Equivalent to:
Schedule.exponential(1000).jitter().attempts(5)
// Quick retry (for fast operations)
Schedule.quick()
// Equivalent to:
Schedule.attempts(3).immediate()
// Standard retry
Schedule.standard()
// Equivalent to:
Schedule.exponential(1000).attempts(5)
// Safe retry (for idempotent operations)
Schedule.safe()
// Equivalent to:
Schedule.spaced(1000).attempts(5)Advanced Usage
Conditional Retry
import { retry, Schedule } from '@deessejs/functions';
// Only retry network errors
const schedule = Schedule.exponential(1000).attempts(5).until((attempt, error) => {
// Don't retry authentication errors
if (error.code === 'AUTH_INVALID') {
return true; // Stop
}
// Retry network errors
return false;
});
const fetchConditional = retry(fetchUsers, schedule);Retry with Maximum Delay
import { Schedule } from '@deessejs/functions';
// Cap maximum delay at 30 seconds
const schedule = Schedule.exponential(1000)
.maxDelay(30000)
.attempts(10);
// Delays: 1000ms, 2000ms, 4000ms, 8000ms, 16000ms, 30000ms, 30000ms, ...Compose Multiple Strategies
import { Schedule } from '@deessejs/functions';
// Use exponential, but capped at spaced intervals
const schedule = Schedule.exponential(1000)
.or(Schedule.spaced(5000)) // Use shorter of exponential or 5s
.attempts(5);
const fetchComposed = retry(fetchUsers, schedule);Type Safety
Retry preserves function signatures:
import { retry, Schedule } from '@deessejs/functions';
async function fetchUser(id: number): Promise<User> {
// ...
}
// ReturnType is preserved: (id: number) => Promise<User>
const fetchUserWithRetry = retry(fetchUser, Schedule.network());
// TypeScript knows types
const user: User = await fetchUserWithRetry(123);Error Handling
Last Error is Thrown
If all retries fail, the last error is thrown:
import { retry, Schedule } from '@deessejs/functions';
try {
await retry(fetchUsers, Schedule.attempts(3));
} catch (error) {
console.error('All retries failed:', error.message);
}Inspect Retry Attempts
import { retry, Schedule } from '@deessejs/functions';
let attemptCount = 0;
const schedule = Schedule.exponential(1000)
.attempts(5)
.tap((attempt) => {
attemptCount = attempt;
});
try {
await retry(fetchUsers, schedule);
} catch (error) {
console.log(`Failed after ${attemptCount} attempts`);
}Best Practices
1. Use Presets for Common Cases
// ✅ Good - use preset
retry(fetchUsers, Schedule.network());
// ❌ Verbose - recreating preset
retry(fetchUsers, Schedule.exponential(1000).jitter().attempts(5));2. Add Jitter for Distributed Systems
// ✅ Good - avoids thundering herd
retry(fetchUsers, Schedule.exponential(1000).jitter());
// ❌ Bad - synchronized retries in distributed systems
retry(fetchUsers, Schedule.exponential(1000));3. Limit Maximum Attempts
// ✅ Good - bounded retries
retry(fetchUsers, Schedule.exponential(1000).attempts(5));
// ⚠️ Risky - could retry forever
retry(fetchUsers, Schedule.exponential(1000));4. Use Spaced for Idempotent Operations
// ✅ Good - consistent delays for idempotent ops
retry(processPayment, Schedule.spaced(5000).attempts(3));
// ✅ Better - exponential for network requests
retry(fetchUsers, Schedule.exponential(1000).attempts(5));Migration from Old API
The old config-based API is still supported but deprecated:
// ⚠️ Old - verbose config object (deprecated)
retry(fetchUsers, {
maxAttempts: 5,
initialDelay: 1000,
backoffMultiplier: 2,
maxDelay: 30000,
jitter: true,
});
// ✅ New - fluent Schedule API (recommended)
retry(fetchUsers, Schedule.exponential(1000).jitter().attempts(5).maxDelay(30000));See Also
- AsyncResult - For handling async operations that can fail
- Result - For type-safe error handling
- Outcome - For distinguishing business failures from system errors