Error Handling Patterns

Error Handling Patterns

Build resilient applications with robust error handling strategies that gracefully handle failures and provide excellent debugging experiences

Category: design Source: wshobson/agents

What Is This

Error handling patterns are essential strategies and techniques used in software development to detect, manage, and recover from failures in a controlled and predictable manner. Mastering error handling patterns enables developers to build resilient applications that can gracefully handle unexpected conditions, provide helpful error messages, and improve debugging and reliability. This skill covers core error handling approaches such as exceptions, result types, error propagation, graceful degradation, and advanced patterns like retries and circuit breakers. By adopting robust error handling patterns, you can design APIs and systems that remain stable under adverse conditions and deliver a better experience for both users and developers.

Why Use It

Proper error handling is critical for several reasons:

  • Reliability: Applications that handle errors gracefully are less likely to crash or produce incorrect results.
  • Maintainability: Clear error handling logic makes code easier to understand and debug.
  • User Experience: Thoughtful error messages help users recover from problems and reduce frustration.
  • Security: Prevents information leakage and undefined behavior that could be exploited.
  • Scalability: Distributed systems and asynchronous workflows require robust error handling to ensure resilience.

Neglecting error handling can lead to silent failures, data corruption, poor user experience, and increased maintenance costs. By implementing best-practice patterns, developers can anticipate both expected and unexpected failures, recover when possible, and provide the necessary context for troubleshooting.

How to Use It

1. Choose the Appropriate Error Handling Philosophy

  • Exceptions: Use language-specific mechanisms such as try-catch blocks (Java, Python, C#) for exceptional conditions that disrupt normal control flow. Exceptions are best for unexpected or unrecoverable errors.
  • Result Types: Use explicit return types that encapsulate success or failure, such as Result<T, E> in Rust or the Either type in Scala/Haskell. These are ideal for expected errors like input validation failures.
  • Error Codes: In C-style languages, functions often return error codes (integers or enums). This method requires discipline to check and propagate errors at every call site.
  • Option/Maybe Types: For values that may be absent, use types like Option<T> (Rust), Optional<T> (Java), or Maybe (Haskell).

Example: Result Type in Rust

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

2. Categorize Errors

  • Recoverable Errors: These are expected and should be handled gracefully. Examples include network timeouts, missing files, invalid user input, and hitting API rate limits.
  • Unrecoverable Errors: These are typically programming bugs or system failures, such as out-of-memory errors, segmentation faults, or assertion failures. These should terminate the process or panic.

3. Error Propagation

Forward errors to the appropriate handler when they cannot be resolved locally. Many languages support mechanisms for propagating errors:

Example: Error Propagation in Python

def read_file(filename):
    try:
        with open(filename) as f:
            return f.read()
    except FileNotFoundError as e:
        raise e  # Propagate the error to the caller

4. Graceful Degradation

When possible, applications should continue to operate in a reduced capacity rather than failing outright. For example, if a web application cannot load user profile images, it can display a default image instead of breaking the entire page.

5. Advanced Patterns

  • Retry Pattern: Automatically retry operations that may fail transiently, such as network requests.
  • Circuit Breaker Pattern: Prevent repeated failures from overwhelming the system by "opening the circuit" after a threshold is reached.

Example: Retry Pattern in JavaScript

async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            return await fetch(url);
        } catch (error) {
            if (i === retries - 1) throw error;
        }
    }
}

6. Provide Meaningful Error Messages

Error messages should be actionable and provide enough context for users and developers to understand and resolve the issue.

When to Use It

  • When implementing error handling in new features or services
  • While designing error-resilient APIs for internal or external consumers
  • During debugging and troubleshooting of production incidents
  • To improve the reliability and robustness of existing applications
  • When creating error messages that aid both user and developer troubleshooting
  • When implementing retry and circuit breaker logic for fault-tolerant systems
  • For handling errors in async or concurrent code
  • When building distributed systems that require graceful handling of partial failures

Important Notes

  • Always choose the error handling pattern that best fits the language, team preferences, and use case.
  • Avoid swallowing errors silently - always log or propagate them as appropriate.
  • Do not expose sensitive information in user-facing error messages.
  • Regularly review error handling code as part of code reviews to ensure consistency and reliability.
  • Combine multiple patterns when necessary. For example, use result types for business logic and exceptions for unexpected failures.
  • Test error handling code paths, including simulated failures, to ensure the system behaves as expected.
  • Remember that robust error handling is foundational for building reliable, maintainable, and user-friendly software systems.