🧠 ZeroThrow Layers
• ZT – primitives (try
,tryAsync
,ok
,err
)
• Result – combinators (map
,andThen
,match
)
• ZeroThrow – utilities (collect
,enhanceAsync
)
• @zerothrow/* – ecosystem packages (resilience, jest, etc)
ZeroThrow Ecosystem · Packages ⇢
Production-grade resilience patterns for ZeroThrow: retry policies, circuit breakers, and timeouts with full Result<T,E> integration.
Breaking Changes:
-
Policy Hierarchy:
Policy
is now the base type with specific subtypes:-
RetryPolicy
for retry operations -
CircuitBreakerPolicy
for circuit breaker patterns -
TimeoutPolicy
for timeout handling
-
-
Factory Renamed:
Policy
static methods are now onPolicyFactory
-
New Callbacks: Added
onRetry
andonCircuitStateChange
for better observability
// Old (v0.1.x)
import { Policy } from '@zerothrow/resilience';
const retry = Policy.retry(3);
// New (v0.2.0)
import { PolicyFactory } from '@zerothrow/resilience';
const retry = PolicyFactory.retry(3);
npm install @zerothrow/resilience @zerothrow/core
# or: pnpm add @zerothrow/resilience @zerothrow/core
import { PolicyFactory } from '@zerothrow/resilience';
import { ZT } from '@zerothrow/core';
// Create a resilient API call with retry, circuit breaker, and timeout
const resilientFetch = PolicyFactory.compose(
PolicyFactory.retry(3, { backoff: 'exponential', delay: 1000 }),
PolicyFactory.circuitBreaker({ threshold: 5, duration: 60000 }),
PolicyFactory.timeout(5000)
);
const result = await resilientFetch.execute(async () => {
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
});
// Using combinators for elegant error handling
const processed = result
.map(data => ({ ...data, timestamp: Date.now() }))
.tap(data => console.log('Success:', data))
.tapErr(error => console.error('Failed after retries:', error))
.unwrapOr({ fallback: true });
Automatically retry failed operations with configurable backoff strategies.
// Basic retry - 3 attempts with constant 1s delay
const retry = PolicyFactory.retry(3);
// Exponential backoff: 1s, 2s, 4s, 8s...
const retryExp = PolicyFactory.retry(5, {
backoff: 'exponential',
delay: 1000, // Base delay
maxDelay: 30000 // Cap at 30 seconds
});
// Linear backoff: 1s, 2s, 3s, 4s...
const retryLinear = PolicyFactory.retry(4, {
backoff: 'linear',
delay: 1000
});
// Selective retry - only retry specific errors
const retryNetwork = PolicyFactory.retry(3, {
handle: (error) => error.message.includes('ECONNREFUSED')
});
Fail fast when a service is down to prevent cascading failures.
// Open circuit after 5 failures, stay open for 60 seconds
const breaker = PolicyFactory.circuitBreaker({
threshold: 5, // Failures before opening
duration: 60000, // Stay open for 1 minute
onOpen: () => console.log('Circuit opened!'),
onClose: () => console.log('Circuit closed!')
});
// The circuit breaker has three states:
// - Closed: Normal operation, requests pass through
// - Open: All requests fail immediately with CircuitOpenError
// - Half-Open: After duration, one request is allowed to test recovery
Prevent operations from hanging indefinitely.
// Timeout after 5 seconds
const timeout = PolicyFactory.timeout(5000);
// Or with options object
const timeout = PolicyFactory.timeout({ timeout: 5000 });
// Operations that exceed the timeout will fail with TimeoutError
const result = await timeout.execute(async () => {
await slowDatabaseQuery(); // Fails if > 5s
});
Combine multiple policies for defense in depth. Policies compose from left to right (leftmost is outermost).
// Method 1: Using compose
const resilient = PolicyFactory.compose(
PolicyFactory.retry(3, { backoff: 'exponential' }),
PolicyFactory.circuitBreaker({ threshold: 5, duration: 60000 }),
PolicyFactory.timeout(5000)
);
// Method 2: Using wrap (for two policies)
const retryWithTimeout = PolicyFactory.wrap(
PolicyFactory.retry(3),
PolicyFactory.timeout(5000)
);
// Execution order (for compose example):
// 1. Retry policy executes
// 2. For each retry attempt:
// - Circuit breaker checks if open
// - If closed, timeout policy executes
// - If timeout succeeds, operation runs
All policies return typed errors that provide context about failures:
import {
RetryExhaustedError,
CircuitOpenError,
TimeoutError
} from '@zerothrow/resilience';
const result = await policy.execute(operation);
if (!result.ok) {
if (result.error instanceof RetryExhaustedError) {
console.log(`Failed after ${result.error.attempts} attempts`);
console.log(`Last error: ${result.error.lastError.message}`);
} else if (result.error instanceof CircuitOpenError) {
console.log(`Circuit opened at ${result.error.openedAt}`);
console.log(`Failure count: ${result.error.failureCount}`);
} else if (result.error instanceof TimeoutError) {
console.log(`Timed out after ${result.error.elapsed}ms`);
}
}
import { PolicyFactory } from '@zerothrow/resilience';
// Create a reusable HTTP client with resilience
class ResilientHttpClient {
private policy = PolicyFactory.compose(
PolicyFactory.retry(3, {
backoff: 'exponential',
handle: (error) => {
// Only retry network and 5xx errors
return error.code === 'ECONNREFUSED' ||
(error.status >= 500 && error.status < 600);
}
}),
PolicyFactory.circuitBreaker({
threshold: 10,
duration: 30000
}),
PolicyFactory.timeout(10000)
);
async get<T>(url: string): Promise<Result<T, Error>> {
return this.policy.execute(async () => {
const response = await fetch(url);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`);
(error as any).status = response.status;
throw error;
}
return response.json() as T;
});
}
}
// Resilient database connection with retry and timeout
const dbPolicy = PolicyFactory.compose(
PolicyFactory.retry(5, {
backoff: 'linear',
delay: 500,
handle: (error) => error.code === 'ECONNREFUSED'
}),
PolicyFactory.timeout(30000)
);
async function queryDatabase(sql: string) {
const result = await dbPolicyFactory.execute(async () => {
const conn = await getConnection();
return conn.query(sql);
});
return result
.tap(rows => logger.debug(`Query returned ${rows.length} rows`))
.tapErr(error => logger.error('Database query failed', { sql, error }))
.map(rows => rows.filter(row => row.active))
.unwrapOr([]);
}
// Different policies for different service criticality
const criticalServicePolicy = PolicyFactory.compose(
PolicyFactory.retry(5, { backoff: 'exponential', maxDelay: 10000 }),
PolicyFactory.circuitBreaker({ threshold: 3, duration: 60000 }),
PolicyFactory.timeout(5000)
);
const nonCriticalServicePolicy = PolicyFactory.compose(
PolicyFactory.retry(1), // Only one retry
PolicyFactory.timeout(2000) // Shorter timeout
);
// Use appropriate policy based on service with result chaining
async function callService(name: string, request: any) {
const policy = name === 'payment'
? criticalServicePolicy
: nonCriticalServicePolicy;
return policy.execute(() => serviceClient.call(name, request))
.then(result => result
.tap(response => metrics.recordLatency(name, response.latency))
.map(response => response.data)
.mapErr(error => {
telemetry.recordError(name, error);
return new ServiceError(name, error);
})
);
}
import { Policy, TestClock } from '@zerothrow/resilience';
// Use TestClock for deterministic tests
test('retry with exponential backoff', async () => {
const clock = new TestClock();
const retry = PolicyFactory.retry(3, {
backoff: 'exponential',
delay: 1000
}, clock);
let attempts = 0;
const operation = jest.fn(async () => {
attempts++;
if (attempts < 3) throw new Error('Failed');
return 'success';
});
const promise = retry.execute(operation);
// First attempt fails immediately
await clock.advance(0);
expect(operation).toHaveBeenCalledTimes(1);
// Second attempt after 1s
await clock.advance(1000);
expect(operation).toHaveBeenCalledTimes(2);
// Third attempt after 2s (exponential)
await clock.advance(2000);
expect(operation).toHaveBeenCalledTimes(3);
const result = await promise;
expect(result.ok).toBe(true);
});
All policies work seamlessly with ZeroThrow's Result type and combinators:
import { ZT, Result, ZeroThrow } from '@zerothrow/core';
import { PolicyFactory } from '@zerothrow/resilience';
// Policies always return Result<T, Error>
const policy = PolicyFactory.retry(3);
const result: Result<Data, Error> = await policy.execute(fetchData);
// Chain multiple transformations
const processed = result
.map(data => transform(data))
.andThen(data => validate(data))
.map(data => enrichData(data))
.tap(data => logger.info('Processing complete', { data }))
.tapErr(error => {
if (error instanceof RetryExhaustedError) {
metrics.increment('retry.exhausted');
}
})
.orElse(error => {
if (error instanceof RetryExhaustedError) {
return ZT.ok(fallbackData);
}
return ZT.err(error);
})
.unwrapOr(defaultData);
// Use with ZT utilities and combinators
const results = await Promise.all(
urls.map(url =>
policy.execute(() => fetch(url).then(r => r.json()))
)
);
// Process all results with combinators
const processed = ZeroThrow.collect(results)
.map(dataArray => dataArray.filter(d => d.valid))
.tap(valid => console.log(`Processed ${valid.length} valid items`))
.unwrapOr([]);
See the main repository for contribution guidelines.
MIT