| name | error-handling-patterns |
| description | This skill should be used when implementing robust error handling strategies - try-catch patterns, error boundaries, retry logic, circuit breakers, and graceful degradation. |
Error Handling Patterns
Overview
Robust error handling is the difference between a system that crashes under pressure and one that gracefully degrades. This skill covers comprehensive error handling strategies including try-catch patterns, error boundaries, retry logic with exponential backoff, circuit breakers, and graceful degradation techniques.
Core Principle: Errors are inevitable. The goal is not to prevent all errors, but to handle them gracefully, recover when possible, and fail safely when recovery isn't possible.
When to Use
Apply these patterns when:
- Building production APIs that must handle failures gracefully
- Implementing client-side applications that consume unreliable external services
- Creating systems that require high availability and fault tolerance
- Working with asynchronous operations, network requests, or third-party integrations
- Building microservices that need to remain resilient when dependencies fail
- Implementing user-facing features that should never crash the entire application
- Setting up monitoring and alerting for production systems
- Designing systems that need to recover from transient failures automatically
Error Handling Strategies
Try-Catch Patterns
Basic Synchronous Error Handling:
function processUserData(data: unknown): User {
try {
// Validate and parse data
const validated = userSchema.parse(data);
// Transform data
return transformToUser(validated);
} catch (error) {
if (error instanceof ZodError) {
// Handle validation errors
throw new ValidationError('Invalid user data', error.errors);
}
// Log unexpected errors
logger.error('Unexpected error processing user data', { error, data });
throw new ProcessingError('Failed to process user data');
}
}
Nested Try-Catch for Specific Error Handling:
async function saveUserWithProfileImage(user: User, imageFile: File): Promise<void> {
let savedUser: User | null = null;
try {
// First operation - save user
try {
savedUser = await database.users.create(user);
} catch (error) {
if (error.code === 'DUPLICATE_KEY') {
throw new ConflictError('User already exists');
}
throw error; // Re-throw unknown errors
}
// Second operation - upload image
try {
const imageUrl = await s3.upload(imageFile, savedUser.id);
await database.users.update(savedUser.id, { profileImage: imageUrl });
} catch (error) {
// Rollback user creation if image upload fails
await database.users.delete(savedUser.id);
throw new ImageUploadError('Failed to upload profile image', { cause: error });
}
} catch (error) {
// Top-level error handling and logging
logger.error('Failed to save user with profile image', {
error,
userId: savedUser?.id,
hasRollback: !!savedUser
});
throw error;
}
}
Error Boundaries (React)
Comprehensive Error Boundary Component:
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
isolate?: boolean; // Prevent error from bubbling up
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Log error to monitoring service
logger.error('React Error Boundary caught error', {
error: error.toString(),
stack: errorInfo.componentStack,
componentStack: errorInfo.componentStack,
});
// Send to error tracking
if (typeof window !== 'undefined' && window.Sentry) {
window.Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
});
}
// Call custom error handler
this.props.onError?.(error, errorInfo);
// Update state
this.setState({ errorInfo });
// Prevent bubbling if isolate is true
if (!this.props.isolate) {
throw error;
}
}
resetError = (): void => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
render(): ReactNode {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="error-boundary-fallback">
<h2>Something went wrong</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error?.toString()}
<br />
{this.state.errorInfo?.componentStack}
</details>
<button onClick={this.resetError}>Try again</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary
fallback={<ErrorFallback />}
onError={(error, info) => analytics.track('ui_error', { error, info })}
>
<Dashboard />
</ErrorBoundary>
);
}
Promise Rejection Handling
Comprehensive Promise Error Handling:
// Avoid: Unhandled promise rejections
async function badExample() {
fetch('/api/data'); // No error handling - dangerous!
}
// Good: Always handle promise rejections
async function goodExample() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new HTTPError(`HTTP ${response.status}: ${response.statusText}`, response.status);
}
return await response.json();
} catch (error) {
if (error instanceof TypeError) {
throw new NetworkError('Network request failed', { cause: error });
}
throw error;
}
}
// Promise chain error handling
function promiseChainExample() {
return fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new HTTPError(`HTTP ${response.status}`, response.status);
}
return response.json();
})
.then(data => processData(data))
.catch(error => {
if (error instanceof HTTPError && error.status === 404) {
return null; // Return default for not found
}
logger.error('Failed to fetch and process data', { error });
throw error;
});
}
// Global unhandled rejection handler
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', (event) => {
logger.error('Unhandled promise rejection', {
reason: event.reason,
promise: event.promise,
});
// Send to error tracking
Sentry.captureException(event.reason);
// Prevent default browser behavior
event.preventDefault();
});
}
Async/Await Error Patterns
Advanced Async Error Handling:
// Pattern 1: Result type for controlled error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function safeAsync<T>(
promise: Promise<T>
): Promise<Result<T>> {
try {
const data = await promise;
return { success: true, data };
} catch (error) {
return { success: false, error: error as Error };
}
}
// Usage
async function fetchUserSafely(id: string) {
const result = await safeAsync(api.getUser(id));
if (!result.success) {
logger.error('Failed to fetch user', { error: result.error, userId: id });
return null;
}
return result.data;
}
// Pattern 2: Parallel async with error aggregation
async function fetchMultipleResources(ids: string[]): Promise<{
successful: Resource[];
failed: Array<{ id: string; error: Error }>;
}> {
const results = await Promise.allSettled(
ids.map(id => api.getResource(id))
);
const successful: Resource[] = [];
const failed: Array<{ id: string; error: Error }> = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successful.push(result.value);
} else {
failed.push({ id: ids[index], error: result.reason });
logger.warn('Resource fetch failed', {
id: ids[index],
error: result.reason
});
}
});
return { successful, failed };
}
Resilience Patterns
Retry with Exponential Backoff
Production-Ready Retry Implementation:
interface RetryOptions {
maxRetries: number;
initialDelay: number;
maxDelay: number;
backoffMultiplier: number;
shouldRetry?: (error: Error, attempt: number) => boolean;
onRetry?: (error: Error, attempt: number, delay: number) => void;
}
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
shouldRetry: (error) => {
// Retry on network errors and 5xx status codes
if (error instanceof NetworkError) return true;
if (error instanceof HTTPError) {
return error.status >= 500 && error.status < 600;
}
return false;
},
};
async function withRetry<T>(
fn: () => Promise<T>,
options: Partial<RetryOptions> = {}
): Promise<T> {
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
let lastError: Error;
for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
// Check if we should retry
const shouldRetry = opts.shouldRetry?.(lastError, attempt) ?? true;
const isLastAttempt = attempt === opts.maxRetries;
if (!shouldRetry || isLastAttempt) {
throw lastError;
}
// Calculate delay with exponential backoff and jitter
const exponentialDelay = Math.min(
opts.initialDelay * Math.pow(opts.backoffMultiplier, attempt - 1),
opts.maxDelay
);
const jitter = exponentialDelay * 0.1 * Math.random();
const delay = exponentialDelay + jitter;
// Call retry callback
opts.onRetry?.(lastError, attempt, delay);
logger.info('Retrying operation', {
attempt,
maxRetries: opts.maxRetries,
delay,
error: lastError.message,
});
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
// Usage
async function fetchDataWithRetry(url: string) {
return withRetry(
() => fetch(url).then(r => r.json()),
{
maxRetries: 5,
initialDelay: 500,
onRetry: (error, attempt, delay) => {
logger.warn('Retrying fetch', { url, attempt, delay, error });
},
}
);
}
Circuit Breaker
Complete Circuit Breaker Implementation:
enum CircuitState {
CLOSED = 'CLOSED', // Normal operation
OPEN = 'OPEN', // Failing, reject requests
HALF_OPEN = 'HALF_OPEN' // Testing if service recovered
}
interface CircuitBreakerOptions {
failureThreshold: number; // Failures before opening
successThreshold: number; // Successes to close from half-open
timeout: number; // Time to wait before half-open (ms)
rollingWindowSize: number; // Window for counting failures
onStateChange?: (from: CircuitState, to: CircuitState) => void;
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failures: number[] = []; // Timestamps of failures
private successes = 0;
private nextAttempt = 0;
constructor(
private name: string,
private options: CircuitBreakerOptions
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() < this.nextAttempt) {
throw new CircuitBreakerOpenError(
`Circuit breaker ${this.name} is OPEN`
);
}
this.setState(CircuitState.HALF_OPEN);
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failures = [];
if (this.state === CircuitState.HALF_OPEN) {
this.successes++;
if (this.successes >= this.options.successThreshold) {
this.setState(CircuitState.CLOSED);
this.successes = 0;
}
}
}
private onFailure(): void {
const now = Date.now();
this.failures.push(now);
this.successes = 0;
// Remove old failures outside rolling window
const windowStart = now - this.options.rollingWindowSize;
this.failures = this.failures.filter(t => t > windowStart);
if (this.failures.length >= this.options.failureThreshold) {
this.setState(CircuitState.OPEN);
this.nextAttempt = now + this.options.timeout;
}
}
private setState(newState: CircuitState): void {
const oldState = this.state;
this.state = newState;
logger.info('Circuit breaker state change', {
name: this.name,
from: oldState,
to: newState,
});
this.options.onStateChange?.(oldState, newState);
}
getState(): CircuitState {
return this.state;
}
reset(): void {
this.failures = [];
this.successes = 0;
this.setState(CircuitState.CLOSED);
}
}
// Usage
const apiCircuitBreaker = new CircuitBreaker('external-api', {
failureThreshold: 5,
successThreshold: 2,
timeout: 60000, // 1 minute
rollingWindowSize: 120000, // 2 minutes
onStateChange: (from, to) => {
metrics.increment('circuit_breaker.state_change', {
name: 'external-api',
from,
to,
});
},
});
async function callExternalAPI(data: unknown) {
return apiCircuitBreaker.execute(async () => {
const response = await fetch('https://api.external.com/data', {
method: 'POST',
body: JSON.stringify(data),
});
if (!response.ok) {
throw new HTTPError(`HTTP ${response.status}`, response.status);
}
return response.json();
});
}
Bulkheads
Bulkhead Pattern for Resource Isolation:
class Bulkhead {
private activeRequests = 0;
private queue: Array<{
resolve: (value: void) => void;
reject: (reason: Error) => void;
}> = [];
constructor(
private maxConcurrent: number,
private maxQueueSize: number = Infinity
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
private async acquire(): Promise<void> {
if (this.activeRequests < this.maxConcurrent) {
this.activeRequests++;
return;
}
if (this.queue.length >= this.maxQueueSize) {
throw new BulkheadRejectError('Bulkhead queue is full');
}
return new Promise((resolve, reject) => {
this.queue.push({ resolve, reject });
});
}
private release(): void {
const next = this.queue.shift();
if (next) {
next.resolve();
} else {
this.activeRequests--;
}
}
}
// Usage: Isolate database connections from API calls
const dbBulkhead = new Bulkhead(10, 50); // Max 10 concurrent, queue 50
const apiBulkhead = new Bulkhead(20, 100);
async function queryDatabase(query: string) {
return dbBulkhead.execute(async () => {
return database.query(query);
});
}
async function callExternalService(url: string) {
return apiBulkhead.execute(async () => {
return fetch(url);
});
}
Timeouts
Timeout Pattern with Cancellation:
class TimeoutError extends Error {
constructor(message: string, public duration: number) {
super(message);
this.name = 'TimeoutError';
}
}
function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
timeoutMessage?: string
): Promise<T> {
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new TimeoutError(
timeoutMessage || `Operation timed out after ${timeoutMs}ms`,
timeoutMs
));
}, timeoutMs);
});
return Promise.race([promise, timeoutPromise]).finally(() => {
clearTimeout(timeoutId);
});
}
// Usage with AbortController for actual cancellation
async function fetchWithTimeout(url: string, timeoutMs: number) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new TimeoutError(`Request to ${url} timed out`, timeoutMs);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
Error Types and Classification
Transient Errors
Identifying and Handling Transient Errors:
class TransientError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
this.name = 'TransientError';
}
}
function isTransientError(error: Error): boolean {
// Network errors are usually transient
if (error instanceof NetworkError) return true;
// HTTP 5xx errors are often transient
if (error instanceof HTTPError) {
return error.status >= 500 && error.status < 600;
}
// Rate limiting is transient
if (error instanceof HTTPError && error.status === 429) return true;
// Database connection errors are often transient
if (error.message.includes('ECONNREFUSED')) return true;
if (error.message.includes('ETIMEDOUT')) return true;
return false;
}
async function handleWithErrorClassification<T>(
fn: () => Promise<T>
): Promise<T> {
try {
return await fn();
} catch (error) {
if (isTransientError(error as Error)) {
// Retry transient errors
return withRetry(fn, { maxRetries: 3 });
}
// Don't retry permanent errors
throw error;
}
}
Permanent Errors
Permanent Error Types:
class PermanentError extends Error {
constructor(message: string, public code: string) {
super(message);
this.name = 'PermanentError';
}
}
class ValidationError extends PermanentError {
constructor(message: string, public errors: unknown[]) {
super(message, 'VALIDATION_ERROR');
}
}
class AuthenticationError extends PermanentError {
constructor(message: string) {
super(message, 'AUTHENTICATION_ERROR');
}
}
class AuthorizationError extends PermanentError {
constructor(message: string, public requiredPermission: string) {
super(message, 'AUTHORIZATION_ERROR');
}
}
class NotFoundError extends PermanentError {
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, 'NOT_FOUND');
}
}
Business Logic Errors
Domain-Specific Error Handling:
class BusinessError extends Error {
constructor(
message: string,
public code: string,
public context?: Record<string, unknown>
) {
super(message);
this.name = 'BusinessError';
}
}
class InsufficientFundsError extends BusinessError {
constructor(required: number, available: number) {
super(
`Insufficient funds: required ${required}, available ${available}`,
'INSUFFICIENT_FUNDS',
{ required, available }
);
}
}
class InventoryError extends BusinessError {
constructor(productId: string, requested: number, available: number) {
super(
`Insufficient inventory for product ${productId}`,
'INSUFFICIENT_INVENTORY',
{ productId, requested, available }
);
}
}
// Usage in business logic
async function processOrder(order: Order): Promise<OrderResult> {
try {
// Check inventory
const inventory = await getInventory(order.productId);
if (inventory.available < order.quantity) {
throw new InventoryError(
order.productId,
order.quantity,
inventory.available
);
}
// Check funds
const balance = await getBalance(order.userId);
if (balance < order.total) {
throw new InsufficientFundsError(order.total, balance);
}
// Process order
return await createOrder(order);
} catch (error) {
if (error instanceof BusinessError) {
// Business errors are expected, handle gracefully
logger.info('Business rule violation', {
code: error.code,
context: error.context,
});
return { success: false, error: error.code, message: error.message };
}
// Unexpected errors should be logged and re-thrown
logger.error('Unexpected error processing order', { error, order });
throw error;
}
}
Logging and Monitoring
Structured Error Logging
Production-Grade Error Logging:
interface ErrorLogContext {
error: Error;
level: 'error' | 'warn' | 'info';
context?: Record<string, unknown>;
user?: { id: string; email?: string };
request?: { method: string; url: string; headers?: Record<string, string> };
}
class ErrorLogger {
log(config: ErrorLogContext): void {
const logEntry = {
timestamp: new Date().toISOString(),
level: config.level,
message: config.error.message,
error: {
name: config.error.name,
message: config.error.message,
stack: config.error.stack,
...this.serializeError(config.error),
},
context: config.context,
user: config.user,
request: config.request,
environment: process.env.NODE_ENV,
service: process.env.SERVICE_NAME,
};
// Send to logging service
console.log(JSON.stringify(logEntry));
// Send to external monitoring
if (config.level === 'error') {
this.sendToMonitoring(logEntry);
}
}
private serializeError(error: Error): Record<string, unknown> {
const serialized: Record<string, unknown> = {};
// Include custom error properties
Object.getOwnPropertyNames(error).forEach(key => {
if (!['name', 'message', 'stack'].includes(key)) {
serialized[key] = (error as any)[key];
}
});
return serialized;
}
private sendToMonitoring(logEntry: unknown): void {
// Integration with monitoring services
// Sentry, Datadog, New Relic, etc.
}
}
const errorLogger = new ErrorLogger();
// Usage in error handlers
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
errorLogger.log({
error,
level: 'error',
context: {
requestId: req.id,
path: req.path,
method: req.method,
},
user: req.user ? { id: req.user.id, email: req.user.email } : undefined,
request: {
method: req.method,
url: req.url,
headers: req.headers as Record<string, string>,
},
});
res.status(500).json({ error: 'Internal server error' });
});
Error Tracking (Sentry Integration)
Sentry Setup with Context:
import * as Sentry from '@sentry/node';
// Initialize Sentry
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
beforeSend(event, hint) {
// Filter sensitive data
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
}
// Add custom logic
return event;
},
});
// Capture errors with context
function captureError(error: Error, context?: Record<string, unknown>): void {
Sentry.withScope((scope) => {
// Add context
if (context) {
Object.entries(context).forEach(([key, value]) => {
scope.setContext(key, value);
});
}
// Add tags for filtering
scope.setTag('error_type', error.name);
// Set error level
if (error instanceof BusinessError) {
scope.setLevel('warning');
} else {
scope.setLevel('error');
}
Sentry.captureException(error);
});
}
Alert Strategies
Smart Alerting Based on Error Patterns:
class AlertManager {
private errorCounts = new Map<string, number[]>();
shouldAlert(error: Error): boolean {
const errorKey = `${error.name}:${error.message}`;
const now = Date.now();
const window = 5 * 60 * 1000; // 5 minutes
// Get recent occurrences
const occurrences = this.errorCounts.get(errorKey) || [];
const recentOccurrences = occurrences.filter(t => t > now - window);
// Add current occurrence
recentOccurrences.push(now);
this.errorCounts.set(errorKey, recentOccurrences);
// Alert if error rate exceeds threshold
const threshold = 10; // 10 errors in 5 minutes
return recentOccurrences.length >= threshold;
}
}
Recovery Strategies
Graceful Degradation
Fallback Implementation:
async function getRecommendations(userId: string): Promise<Product[]> {
try {
// Try ML-based recommendations
return await withTimeout(
mlService.getRecommendations(userId),
2000 // 2 second timeout
);
} catch (error) {
logger.warn('ML recommendations failed, using fallback', { error, userId });
try {
// Fallback to collaborative filtering
return await collaborativeFiltering.getRecommendations(userId);
} catch (error) {
logger.warn('Collaborative filtering failed, using default', { error });
// Final fallback: popular products
return await getPopularProducts();
}
}
}
Fallback Mechanisms
Multi-Level Fallback Pattern:
interface FallbackConfig<T> {
primary: () => Promise<T>;
fallbacks: Array<() => Promise<T>>;
default: T;
onFallback?: (level: number, error: Error) => void;
}
async function withFallback<T>(config: FallbackConfig<T>): Promise<T> {
const attempts = [config.primary, ...config.fallbacks];
for (let i = 0; i < attempts.length; i++) {
try {
return await attempts[i]();
} catch (error) {
config.onFallback?.(i, error as Error);
if (i === attempts.length - 1) {
logger.error('All fallbacks exhausted', { error });
return config.default;
}
}
}
return config.default;
}
// Usage
const userData = await withFallback({
primary: () => cache.get(userId),
fallbacks: [
() => database.users.findById(userId),
() => legacyAPI.getUser(userId),
],
default: { id: userId, name: 'Guest' },
onFallback: (level, error) => {
metrics.increment('user_fetch.fallback', { level });
},
});
Data Recovery
Transaction Rollback and Recovery:
async function performComplexTransaction(data: TransactionData) {
const operations: Array<() => Promise<void>> = [];
try {
// Operation 1: Update inventory
await database.inventory.decrement(data.productId, data.quantity);
operations.push(() =>
database.inventory.increment(data.productId, data.quantity)
);
// Operation 2: Charge payment
const paymentId = await paymentService.charge(data.userId, data.amount);
operations.push(() =>
paymentService.refund(paymentId)
);
// Operation 3: Create order
const orderId = await database.orders.create(data);
operations.push(() =>
database.orders.delete(orderId)
);
// Operation 4: Send confirmation
await emailService.send(data.email, 'Order confirmed');
return { success: true, orderId };
} catch (error) {
logger.error('Transaction failed, rolling back', { error, data });
// Rollback in reverse order
for (const rollback of operations.reverse()) {
try {
await rollback();
} catch (rollbackError) {
logger.error('Rollback operation failed', {
rollbackError,
originalError: error
});
}
}
throw new TransactionError('Transaction failed and was rolled back', {
cause: error,
});
}
}
Anti-Patterns
Common Mistakes to Avoid:
- Silent Failures
// WRONG: Swallowing errors
try {
await riskyOperation();
} catch (error) {
// Nothing - error is lost!
}
// RIGHT: Always log or handle
try {
await riskyOperation();
} catch (error) {
logger.error('Risky operation failed', { error });
throw error; // Or handle appropriately
}
- Generic Error Handlers
// WRONG: Treating all errors the same
catch (error) {
return res.status(500).json({ error: 'Something went wrong' });
}
// RIGHT: Handle different error types
catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({ error: error.message, details: error.errors });
}
if (error instanceof NotFoundError) {
return res.status(404).json({ error: error.message });
}
logger.error('Unexpected error', { error });
return res.status(500).json({ error: 'Internal server error' });
}
- Retry Without Backoff
// WRONG: Immediate retry hammers the service
for (let i = 0; i < 3; i++) {
try {
return await callAPI();
} catch (error) {
if (i === 2) throw error;
}
}
// RIGHT: Use exponential backoff
return withRetry(callAPI, { maxRetries: 3, initialDelay: 1000 });
- Exposing Internal Errors
// WRONG: Leaking implementation details
res.status(500).json({
error: error.stack,
query: 'SELECT * FROM users WHERE password = ...'
});
// RIGHT: Safe error messages
res.status(500).json({
error: 'An unexpected error occurred',
requestId: req.id // For support reference
});
- Missing Error Boundaries
// WRONG: No error boundary
function App() {
return <Dashboard />; // Entire app crashes on error
}
// RIGHT: Isolated error boundaries
function App() {
return (
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
);
}
Examples
Example 1: API Error Handling Middleware
// Express error handling middleware
function errorHandler(
error: Error,
req: Request,
res: Response,
next: NextFunction
): void {
// Log all errors
errorLogger.log({
error,
level: 'error',
context: { requestId: req.id },
request: {
method: req.method,
url: req.url,
},
});
// Handle known error types
if (error instanceof ValidationError) {
res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
return;
}
if (error instanceof AuthenticationError) {
res.status(401).json({
error: 'Authentication required',
});
return;
}
if (error instanceof AuthorizationError) {
res.status(403).json({
error: 'Insufficient permissions',
});
return;
}
if (error instanceof NotFoundError) {
res.status(404).json({
error: error.message,
});
return;
}
if (error instanceof ConflictError) {
res.status(409).json({
error: error.message,
});
return;
}
// Default to 500 for unknown errors
res.status(500).json({
error: 'An unexpected error occurred',
requestId: req.id,
});
}
Example 2: Resilient API Client
class ResilientAPIClient {
private circuitBreaker: CircuitBreaker;
constructor(private baseURL: string) {
this.circuitBreaker = new CircuitBreaker('api-client', {
failureThreshold: 5,
successThreshold: 2,
timeout: 30000,
rollingWindowSize: 60000,
});
}
async get<T>(path: string, options: RequestOptions = {}): Promise<T> {
return this.request<T>('GET', path, options);
}
async post<T>(path: string, data: unknown, options: RequestOptions = {}): Promise<T> {
return this.request<T>('POST', path, { ...options, body: data });
}
private async request<T>(
method: string,
path: string,
options: RequestOptions
): Promise<T> {
// Wrap in circuit breaker
return this.circuitBreaker.execute(async () => {
// Retry with exponential backoff
return withRetry(
async () => {
// Add timeout
return withTimeout(
this.executeRequest<T>(method, path, options),
options.timeout || 10000
);
},
{
maxRetries: 3,
initialDelay: 1000,
shouldRetry: (error) => isTransientError(error),
}
);
});
}
private async executeRequest<T>(
method: string,
path: string,
options: RequestOptions
): Promise<T> {
const url = `${this.baseURL}${path}`;
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
throw new HTTPError(
`HTTP ${response.status}: ${response.statusText}`,
response.status
);
}
return response.json();
}
}
Example 3: Database Query with Error Recovery
async function executeQueryWithRecovery<T>(
query: string,
params: unknown[]
): Promise<T> {
try {
// Try primary database
return await primaryDB.query<T>(query, params);
} catch (error) {
logger.error('Primary database query failed', { error, query });
// Check if error is recoverable
if (isConnectionError(error)) {
logger.info('Attempting to reconnect to database');
await primaryDB.reconnect();
try {
return await primaryDB.query<T>(query, params);
} catch (retryError) {
logger.error('Retry on primary DB failed', { retryError });
}
}
// Try read replica for SELECT queries
if (query.trim().toUpperCase().startsWith('SELECT')) {
logger.info('Attempting query on read replica');
try {
return await replicaDB.query<T>(query, params);
} catch (replicaError) {
logger.error('Read replica query failed', { replicaError });
}
}
// All recovery attempts failed
throw new DatabaseError('All database query attempts failed', {
cause: error,
});
}
}
Example 4: React Data Fetching with Error Handling
function useDataWithErrorHandling<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
let cancelled = false;
async function fetchData() {
setLoading(true);
setError(null);
try {
const result = await withRetry(
async () => {
const response = await fetch(url);
if (!response.ok) {
throw new HTTPError(`HTTP ${response.status}`, response.status);
}
return response.json();
},
{
maxRetries: 3,
initialDelay: 1000,
onRetry: (error, attempt) => {
console.log(`Retry attempt ${attempt}`, error);
},
}
);
if (!cancelled) {
setData(result);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
// Log to monitoring
captureError(err as Error, { url, retryCount });
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url, retryCount]);
const retry = useCallback(() => {
setRetryCount(count => count + 1);
}, []);
return { data, error, loading, retry };
}
// Usage in component
function DataDisplay() {
const { data, error, loading, retry } = useDataWithErrorHandling('/api/data');
if (loading) return <LoadingSpinner />;
if (error) {
return (
<ErrorMessage>
<p>Failed to load data: {error.message}</p>
<button onClick={retry}>Retry</button>
</ErrorMessage>
);
}
return <DataView data={data} />;
}
Example 5: Comprehensive Error Handling in Async Operations
async function processUserRegistration(userData: UserRegistrationData) {
const operations: Array<{ name: string; rollback: () => Promise<void> }> = [];
try {
// Step 1: Validate input
const validated = await validateUserData(userData);
// Step 2: Create user account
const user = await database.users.create(validated);
operations.push({
name: 'create_user',
rollback: async () => {
await database.users.delete(user.id);
logger.info('Rolled back user creation', { userId: user.id });
},
});
// Step 3: Create auth credentials
const authId = await authService.createCredentials(user.id, validated.password);
operations.push({
name: 'create_auth',
rollback: async () => {
await authService.deleteCredentials(authId);
logger.info('Rolled back auth creation', { authId });
},
});
// Step 4: Send welcome email (with retry)
await withRetry(
() => emailService.sendWelcome(user.email),
{
maxRetries: 3,
initialDelay: 1000,
shouldRetry: (error) => error instanceof NetworkError,
}
);
// Step 5: Create initial profile
await database.profiles.create({
userId: user.id,
displayName: validated.displayName,
});
logger.info('User registration completed', { userId: user.id });
return { success: true, userId: user.id };
} catch (error) {
logger.error('User registration failed', { error, userData: userData.email });
// Rollback all operations in reverse order
for (const op of operations.reverse()) {
try {
await op.rollback();
} catch (rollbackError) {
logger.error('Rollback failed', {
operation: op.name,
rollbackError,
originalError: error,
});
// Send alert for failed rollback
await alerting.sendCritical('Rollback failed during user registration', {
operation: op.name,
error: rollbackError,
});
}
}
// Classify and throw appropriate error
if (error instanceof ValidationError) {
throw error;
}
if (error.code === 'DUPLICATE_EMAIL') {
throw new ConflictError('Email already registered');
}
throw new RegistrationError('Failed to complete user registration', {
cause: error,
});
}
}