| name | error-handling |
| description | Error handling patterns, strategies, and implementation. Use when user asks to "handle errors", "add error handling", "create custom errors", "handle exceptions", "implement retry logic", "add error boundaries", "design error responses", "handle async errors", "create error types", "implement graceful degradation", "error recovery", "fallback strategies", "exponential backoff", "timeout handling", "error logging", "error monitoring", or mentions error handling patterns, exception handling, error boundaries, retry strategies, circuit breakers, or error recovery. |
| license | MIT |
| metadata | {"author":"1mangesh1","version":"1.0.0","tags":["error-handling","exceptions","retry","circuit-breaker","resilience","fault-tolerance"]} |
Error Handling Patterns & Best Practices
Comprehensive error handling strategies across languages and frameworks for building resilient, fault-tolerant software systems.
Error Handling Philosophy
Fail Fast
Detect problems as early as possible and report them immediately. Do not let invalid state propagate through the system.
- Validate inputs at system boundaries
- Use assertions for invariants during development
- Throw on impossible states rather than returning defaults
- Prefer compile-time checks over runtime checks
Fail Gracefully
When failure is unavoidable, degrade functionality rather than crashing the entire system.
- Provide fallback values or cached responses
- Shed load intelligently under pressure
- Communicate errors clearly to the caller
- Preserve as much functionality as possible
Operational vs Programmer Errors
| Aspect | Operational Errors | Programmer Errors |
|---|
| Cause | Runtime conditions (network, disk) | Bugs in code (null ref, bad logic) |
| Predictable? | Yes | No |
| Handling | Retry, fallback, alert | Fix the code |
| Examples | Timeout, DNS failure, disk full | TypeError, off-by-one, null deref |
| Recovery | Automated or manual intervention | Deploy a fix |
JavaScript / TypeScript Error Handling
Basic try/catch
try {
const data = JSON.parse(rawInput);
processData(data);
} catch (error: unknown) {
if (error instanceof SyntaxError) {
console.error("Invalid JSON:", error.message);
} else if (error instanceof TypeError) {
console.error("Type error during processing:", error.message);
} else {
throw error;
}
}
Async/Await Error Handling
async function fetchUserData(userId: string): Promise<User> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new HttpError(response.status, `Failed to fetch user: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
throw new NetworkError("Unable to reach the server", { cause: error });
}
}
Promise.allSettled for Concurrent Operations
async function fetchMultipleResources(ids: string[]) {
const results = await Promise.allSettled(
ids.map(id => fetchResource(id))
);
const successes: Resource[] = [];
const failures: { id: string; reason: unknown }[] = [];
results.forEach((result, index) => {
if (result.status === "fulfilled") {
successes.push(result.value);
} else {
failures.push({ id: ids[index], reason: result.reason });
}
});
if (failures.length > 0) {
console.warn(`Failed to fetch ${failures.length}/${ids.length} resources`);
}
return { successes, failures };
}
Custom Error Classes (TypeScript)
class AppError extends Error {
public readonly code: string;
public readonly statusCode: number;
public readonly isOperational: boolean;
public readonly context?: Record<string, unknown>;
constructor(options: {
message: string;
code: string;
statusCode?: number;
isOperational?: boolean;
cause?: Error;
context?: Record<string, unknown>;
}) {
super(options.message, { cause: options.cause });
this.name = this.constructor.name;
this.code = options.code;
this.statusCode = options.statusCode ?? 500;
this.isOperational = options.isOperational ?? true;
this.context = options.context;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super({
message: `${resource} with id '${id}' not found`,
code: "RESOURCE_NOT_FOUND",
statusCode: 404,
context: { resource, id },
});
}
}
class ValidationError extends AppError {
public readonly fields: Record<string, string[]>;
constructor(fields: Record<string, string[]>) {
const fieldNames = Object.keys(fields).join(", ");
super({
message: `Validation failed for fields: ${fieldNames}`,
code: "VALIDATION_ERROR",
statusCode: 422,
context: { fields },
});
this.fields = fields;
}
}
class ConflictError extends AppError {
constructor(message: string) {
super({
message,
code: "CONFLICT",
statusCode: 409,
});
}
}
Global Error Handlers (Node.js)
process.on("unhandledRejection", (reason: unknown, promise: Promise<unknown>) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
monitor.captureException(reason);
});
process.on("uncaughtException", (error: Error) => {
console.error("Uncaught Exception:", error);
monitor.captureException(error);
process.exit(1);
});
process.on("SIGTERM", async () => {
console.log("SIGTERM received. Starting graceful shutdown...");
await server.close();
await database.disconnect();
process.exit(0);
});
Python Exception Handling
try/except/else/finally
def read_config(path: str) -> dict:
"""Read and parse a JSON configuration file."""
try:
with open(path, "r") as f:
raw = f.read()
except FileNotFoundError:
raise ConfigError(f"Config file not found: {path}")
except PermissionError:
raise ConfigError(f"Permission denied reading: {path}")
else:
try:
return json.loads(raw)
except json.JSONDecodeError as e:
raise ConfigError(f"Invalid JSON in {path}: {e}")
finally:
logger.debug(f"Finished config read attempt for {path}")
Custom Exception Hierarchy
class AppError(Exception):
"""Base exception for the application."""
def __init__(self, message: str, code: str, status_code: int = 500,
context: dict | None = None):
super().__init__(message)
self.message = message
self.code = code
self.status_code = status_code
self.context = context or {}
self.is_operational = True
def to_dict(self) -> dict:
return {
"error": {
"type": self.code,
"title": self.__class__.__name__,
"detail": self.message,
"status": self.status_code,
**({k: v for k, v in self.context.items()} if self.context else {}),
}
}
class NotFoundError(AppError):
def __init__(self, resource: str, identifier: str):
super().__init__(
message=f"{resource} '{identifier}' not found",
code="RESOURCE_NOT_FOUND",
status_code=404,
context={"resource": resource, "identifier": identifier},
)
class ValidationError(AppError):
def __init__(self, errors: dict[str, list[str]]):
super().__init__(
message="Validation failed",
code="VALIDATION_ERROR",
status_code=422,
context={"fields": errors},
)
self.field_errors = errors
class RateLimitError(AppError):
def __init__(self, retry_after: int):
super().__init__(
message="Rate limit exceeded",
code="RATE_LIMIT_EXCEEDED",
status_code=429,
context={"retry_after": retry_after},
)
Context Managers for Error Handling
from contextlib import contextmanager
@contextmanager
def error_boundary(operation: str, reraise: bool = True):
"""Context manager that logs errors and optionally suppresses them."""
try:
yield
except AppError:
raise
except Exception as exc:
logger.exception(f"Unexpected error during {operation}")
monitor.capture_exception(exc)
if reraise:
raise AppError(
message=f"Internal error during {operation}",
code="INTERNAL_ERROR",
) from exc
with error_boundary("user_creation"):
user = create_user(data)
Go Error Handling
Error Interface and Wrapping
package apperr
import (
"errors"
"fmt"
)
type AppError struct {
Code string
Message string
StatusCode int
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
var (
ErrNotFound = &AppError{Code: "NOT_FOUND", StatusCode: 404}
ErrUnauthorized = &AppError{Code: "UNAUTHORIZED", StatusCode: 401}
ErrConflict = &AppError{Code: "CONFLICT", StatusCode: 409}
)
func NewNotFound(resource, id string) error {
return &AppError{
Code: "NOT_FOUND",
Message: fmt.Sprintf("%s '%s' not found", resource, id),
StatusCode: 404,
}
}
func Wrap(err error, message string) error {
return fmt.Errorf("%s: %w", message, err)
}
errors.Is and errors.As
func GetUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, NewNotFound("user", id)
}
return nil, Wrap(err, "failed to query user")
}
return user, nil
}
func handleError(err error) {
var appErr *AppError
if errors.As(err, &appErr) {
respondWithError(appErr.StatusCode, appErr.Code, appErr.Message)
} else {
log.Printf("unexpected error: %v", err)
respondWithError(500, "INTERNAL_ERROR", "An internal error occurred")
}
}
Java Exception Handling
Checked vs Unchecked Exceptions
public class ResourceNotFoundException extends Exception {
private final String resource;
private final String identifier;
public ResourceNotFoundException(String resource, String identifier) {
super(String.format("%s '%s' not found", resource, identifier));
this.resource = resource;
this.identifier = identifier;
}
public String getResource() { return resource; }
public String getIdentifier() { return identifier; }
}
public class InvalidStateException extends RuntimeException {
public InvalidStateException(String message) {
super(message);
}
}
try-with-resources
public List<User> readUsersFromCsv(Path path) throws UserImportException {
List<User> users = new ArrayList<>();
try (
var reader = Files.newBufferedReader(path);
var csvParser = new CSVParser(reader, CSVFormat.DEFAULT.withHeader())
) {
for (CSVRecord record : csvParser) {
users.add(parseUser(record));
}
} catch (IOException e) {
throw new UserImportException("Failed to read CSV file: " + path, e);
} catch (IllegalArgumentException e) {
throw new UserImportException("Malformed CSV record in: " + path, e);
}
return users;
}
Rust Error Handling
Result, Option, and the ? Operator
use std::fs;
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Resource '{resource}' with id '{id}' not found")]
NotFound { resource: String, id: String },
#[error("Validation failed: {0}")]
Validation(String),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Internal error: {0}")]
Internal(String),
}
impl AppError {
pub fn status_code(&self) -> u16 {
match self {
AppError::NotFound { .. } => 404,
AppError::Validation(_) => 422,
AppError::Database(_) => 500,
AppError::Io(_) => 500,
AppError::Serialization(_) => 400,
AppError::Internal(_) => 500,
}
}
}
fn read_config(path: &str) -> Result<Config, AppError> {
let contents = fs::read_to_string(path)?;
let config: Config = serde_json::from_str(&contents)?;
Ok(config)
}
Using anyhow for Application Code
use anyhow::{Context, Result};
fn process_file(path: &str) -> Result<()> {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {path}"))?;
let data: serde_json::Value = serde_json::from_str(&contents)
.with_context(|| format!("Invalid JSON in file: {path}"))?;
process_data(&data)
.context("Failed to process data")?;
Ok(())
}
React Error Boundaries
import React, { Component, ErrorInfo, ReactNode } from "react";
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("Error Boundary caught:", error, errorInfo);
this.props.onError?.(error, errorInfo);
}
render(): ReactNode {
if (this.state.hasError) {
return this.props.fallback ?? (
<div role="alert">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary
fallback={<p>Dashboard failed to load.</p>}
onError={(error) => monitor.captureException(error)}
>
<Dashboard />
</ErrorBoundary>
);
}
Express / Koa Error Middleware
Express Error Middleware
import { Request, Response, NextFunction } from "express";
function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get("/api/users/:id", asyncHandler(async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError("User", req.params.id);
}
res.json(user);
}));
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
if (err instanceof AppError && err.isOperational) {
res.status(err.statusCode).json({
type: `https://api.example.com/errors/${err.code}`,
title: err.name,
status: err.statusCode,
detail: err.message,
instance: req.originalUrl,
...(err.context ?? {}),
});
} else {
console.error("Unhandled error:", err);
monitor.captureException(err);
res.status(500).json({
type: "https://api.example.com/errors/INTERNAL_ERROR",
title: "Internal Server Error",
status: 500,
detail: "An unexpected error occurred",
instance: req.originalUrl,
});
}
});
Koa Error Middleware
import Koa from "koa";
const app = new Koa();
app.use(async (ctx, next) => {
try {
await next();
} catch (err: any) {
if (err instanceof AppError && err.isOperational) {
ctx.status = err.statusCode;
ctx.body = {
type: `https://api.example.com/errors/${err.code}`,
title: err.name,
status: err.statusCode,
detail: err.message,
};
} else {
console.error("Unhandled error:", err);
ctx.status = 500;
ctx.body = {
type: "https://api.example.com/errors/INTERNAL_ERROR",
title: "Internal Server Error",
status: 500,
detail: "An unexpected error occurred",
};
}
}
});
Error Serialization for APIs (RFC 7807)
Problem Details (RFC 7807 / RFC 9457)
Standardized format for HTTP API error responses.
interface ProblemDetail {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
[key: string]: unknown;
}
{
"type": "https://api.example.com/errors/VALIDATION_ERROR",
"title": "Validation Error",
"status": 422,
"detail": "The request body contains invalid fields",
"instance": "/api/users",
"errors": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be a positive integer" }
]
}
{
"type": "https://api.example.com/errors/RATE_LIMIT_EXCEEDED",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded the rate limit of 100 requests per minute",
"instance": "/api/search",
"retryAfter": 32
}
Error Codes and Error Catalogs
Define a centralized catalog of error codes so clients can programmatically handle errors.
export const ErrorCatalog = {
AUTH_001: {
code: "AUTH_001",
title: "Invalid Credentials",
statusCode: 401,
description: "The provided username or password is incorrect.",
},
AUTH_002: {
code: "AUTH_002",
title: "Token Expired",
statusCode: 401,
description: "The authentication token has expired. Please re-authenticate.",
},
AUTH_003: {
code: "AUTH_003",
title: "Insufficient Permissions",
statusCode: 403,
description: "You do not have permission to perform this action.",
},
RES_001: {
code: "RES_001",
title: "Resource Not Found",
statusCode: 404,
description: "The requested resource does not exist.",
},
VAL_001: {
code: "VAL_001",
title: "Validation Error",
statusCode: 422,
description: "One or more fields in the request are invalid.",
},
RATE_001: {
code: "RATE_001",
title: "Rate Limit Exceeded",
statusCode: 429,
description: "Too many requests. Please retry after the specified interval.",
},
SYS_001: {
code: "SYS_001",
title: "Service Unavailable",
statusCode: 503,
description: "The service is temporarily unavailable. Please try again later.",
},
} as const;
type ErrorCode = keyof typeof ErrorCatalog;
function createError(code: ErrorCode, detail?: string): AppError {
const entry = ErrorCatalog[code];
return new AppError({
message: detail ?? entry.description,
code: entry.code,
statusCode: entry.statusCode,
});
}
Retry Patterns
Exponential Backoff with Jitter
interface RetryOptions {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
jitter: boolean;
retryableErrors?: (error: unknown) => boolean;
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
}
const DEFAULT_OPTIONS: RetryOptions = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30_000,
jitter: true,
};
async function withRetry<T>(
fn: () => Promise<T>,
options: Partial<RetryOptions> = {}
): Promise<T> {
const opts = { ...DEFAULT_OPTIONS, ...options };
let lastError: unknown;
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (opts.retryableErrors && !opts.retryableErrors(error)) {
throw error;
}
if (attempt === opts.maxRetries) {
break;
}
let delay = Math.min(
opts.baseDelayMs * Math.pow(2, attempt),
opts.maxDelayMs
);
if (opts.jitter) {
delay = delay * (0.5 + Math.random() * 0.5);
}
opts.onRetry?.(error, attempt + 1, delay);
await sleep(delay);
}
}
throw lastError;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
const data = await withRetry(
() => fetchFromApi("/data"),
{
maxRetries: 5,
baseDelayMs: 500,
retryableErrors: (err) => err instanceof NetworkError || (err instanceof HttpError && err.status >= 500),
onRetry: (err, attempt, delay) => {
console.warn(`Retry attempt ${attempt} after ${delay}ms due to: ${err}`);
},
}
);
Circuit Breaker Pattern
Prevent cascading failures by stopping calls to a failing service.
enum CircuitState {
CLOSED = "CLOSED",
OPEN = "OPEN",
HALF_OPEN = "HALF_OPEN",
}
interface CircuitBreakerOptions {
failureThreshold: number;
resetTimeoutMs: number;
successThreshold: number;
monitorInterval?: number;
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount = 0;
private successCount = 0;
private lastFailureTime = 0;
constructor(
private readonly name: string,
private readonly options: CircuitBreakerOptions
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime >= this.options.resetTimeoutMs) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
throw new CircuitBreakerOpenError(
`Circuit breaker '${this.name}' is open. Service unavailable.`
);
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.options.successThreshold) {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
console.log(`Circuit breaker '${this.name}' closed. Service recovered.`);
}
} else {
this.failureCount = 0;
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = CircuitState.OPEN;
console.warn(`Circuit breaker '${this.name}' opened after ${this.failureCount} failures.`);
}
}
getState(): CircuitState {
return this.state;
}
}
class CircuitBreakerOpenError extends Error {
constructor(message: string) {
super(message);
this.name = "CircuitBreakerOpenError";
}
}
const paymentCircuit = new CircuitBreaker("payment-service", {
failureThreshold: 5,
resetTimeoutMs: 30_000,
successThreshold: 3,
});
async function processPayment(order: Order) {
return paymentCircuit.execute(() => paymentApi.charge(order));
}
Graceful Degradation and Fallbacks
interface FallbackOptions<T> {
primary: () => Promise<T>;
fallback: () => Promise<T>;
shouldFallback?: (error: unknown) => boolean;
onFallback?: (error: unknown) => void;
}
async function withFallback<T>(options: FallbackOptions<T>): Promise<T> {
try {
return await options.primary();
} catch (error) {
if (options.shouldFallback && !options.shouldFallback(error)) {
throw error;
}
options.onFallback?.(error);
return options.fallback();
}
}
const recommendations = await withFallback({
primary: () => mlService.getRecommendations(userId),
fallback: () => cache.get(`recommendations:${userId}`) ?? getPopularItems(),
shouldFallback: (err) => err instanceof NetworkError || err instanceof TimeoutError,
onFallback: (err) => console.warn("ML service unavailable, using cached recommendations:", err),
});
async function getProductPrice(productId: string): Promise<number> {
try {
return await pricingService.getPrice(productId);
} catch {
try {
const cached = await cache.get(`price:${productId}`);
if (cached) return cached;
} catch {
}
const product = await db.products.findById(productId);
if (!product) throw new NotFoundError("Product", productId);
return product.basePrice;
}
}
Error Logging and Monitoring Integration
import { Logger } from "./logger";
import { Monitor } from "./monitor";
interface ErrorReporter {
report(error: unknown, context?: Record<string, unknown>): void;
}
class ErrorHandler implements ErrorReporter {
constructor(
private readonly logger: Logger,
private readonly monitor: Monitor,
) {}
report(error: unknown, context?: Record<string, unknown>): void {
if (error instanceof AppError && error.isOperational) {
this.logger.warn("Operational error", {
code: error.code,
message: error.message,
statusCode: error.statusCode,
...context,
});
this.monitor.incrementCounter("operational_errors", {
code: error.code,
});
} else {
this.logger.error("Unexpected error", {
error: error instanceof Error ? error.stack : String(error),
...context,
});
this.monitor.captureException(error, context);
this.monitor.incrementCounter("unexpected_errors");
}
}
middleware() {
return (err: Error, req: Request, res: Response, next: NextFunction) => {
this.report(err, {
method: req.method,
path: req.path,
query: req.query,
userId: (req as any).userId,
requestId: req.headers["x-request-id"],
});
if (err instanceof AppError) {
res.status(err.statusCode).json(err.toJSON());
} else {
res.status(500).json({
type: "https://api.example.com/errors/INTERNAL_ERROR",
title: "Internal Server Error",
status: 500,
detail: "An unexpected error occurred",
});
}
};
}
}
Summary of Best Practices
- Use typed, structured errors - Create error hierarchies with codes, status codes, and context.
- Separate operational errors from programmer errors - Handle them differently in monitoring and recovery.
- Always wrap lower-level errors - Add context at each layer (what operation failed) while preserving the root cause.
- Use RFC 7807 Problem Details for API error responses - Consistent, machine-readable format.
- Implement retries with exponential backoff and jitter - Protect against transient failures and thundering herds.
- Use circuit breakers for external service calls - Prevent cascading failures.
- Design fallback paths - Graceful degradation is better than total failure.
- Centralize error handling - Use middleware, global handlers, and a unified error reporter.
- Log structured data - Include request IDs, user context, and error codes for debugging.
- Define an error catalog - Enumerate all known error codes so clients can handle them programmatically.
- Fail fast at boundaries - Validate early, throw early, catch at the appropriate level.
- Never swallow errors silently - If you catch an exception, log it or re-throw it.
- Use language-idiomatic patterns - Result types in Rust, error interface in Go, custom exceptions in Python/Java.
- Test error paths - Write tests for failure scenarios, not just the happy path.
- Monitor error rates and set alerts - Track operational error frequency and latency impact.