home / Intermediate / Error Handling Patterns

Error Handling Patterns

level: intermediate · difficulty 2/3 · published 2026-10-02

What it is

Error handling patterns are structured ways to detect, report, and recover from runtime problems in a program. Instead of letting an error crash the process or silently corrupt state, these patterns define a consistent flow: where an error is raised, how it propagates, who is responsible for catching it, and what the caller sees. Common patterns include return codes, exceptions, result types (such as Result in Rust or Either in Haskell), and callbacks with error-first arguments (common in Node.js). Each pattern makes different trade-offs between explicitness, performance, and control flow. For example, return codes force the caller to check every call, while exceptions separate the happy path from error handling but can be thrown from unexpected places. A pattern is not a single function; it is a convention shared across a codebase so that errors are handled predictably. Understanding these patterns lets you choose the right tool for a given language and situation, rather than mixing incompatible styles.

Why it matters

Errors are inevitable: files go missing, networks time out, users enter invalid input, and assumptions fail. Without a deliberate pattern, error handling becomes ad hoc, leading to swallowed exceptions, inconsistent logging, and crashes that are hard to reproduce. A good pattern makes failures visible and recoverable at the right level. It also improves maintainability: when every module follows the same convention, developers know where to look and what to expect. In concurrent or distributed systems, clear error propagation prevents partial updates and resource leaks. Moreover, error handling affects performance and API design. Exceptions can be expensive when thrown frequently, while return codes add branching noise. Choosing a pattern early avoids costly rewrites. Finally, many languages and frameworks have idiomatic patterns; following them makes your code easier for others to read and integrates better with libraries. Ignoring error handling is a leading cause of production incidents, so treating it as a first-class design concern pays off.

How it works under the hood

Under the hood, error handling mechanisms rely on language runtime features. Exceptions typically use a stack unwinding process: when an exception is thrown, the runtime searches up the call stack for a matching catch block, running destructors or finally blocks along the way. This can involve a table of exception handlers and may be optimized to have zero cost when no exception is thrown (as in C++ and Java). Return codes are just values returned from functions; the caller must inspect them, often using an if statement or a match expression. Result types are algebraic data types that encode success or failure as distinct cases, forcing the caller to handle both via pattern matching. Error-first callbacks pass an error object as the first argument to a callback function, a convention popularized by Node.js. In all cases, the key is that error information is carried explicitly in the control flow or data, rather than being ignored. Some languages combine patterns, like Go returning error values and also supporting panic/recover for exceptional cases.

A worked example

Consider a function that reads a configuration file and parses it. Using exceptions in Python, you might write:

def load_config(path):
    try:
        with open(path) as f:
            data = f.read()
        return parse_config(data)
    except FileNotFoundError:
        return default_config()
    except ParseError as e:
        raise ConfigError(f"Invalid config: {e}")

Here, FileNotFoundError is caught and handled by returning a default, while ParseError is wrapped in a custom ConfigError to give the caller more context. The caller can then decide whether to retry, log, or exit. In a language like Go, the same logic uses explicit error returns:

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        if os.IsNotExist(err) {
            return DefaultConfig(), nil
        }
        return nil, err
    }
    cfg, err := ParseConfig(data)
    if err != nil {
        return nil, fmt.Errorf("invalid config: %w", err)
    }
    return cfg, nil
}

Both patterns achieve the same goal: handle missing files gracefully and propagate parse errors with context. The choice depends on language idioms and team preference.

Common mistakes

A frequent mistake is catching exceptions too broadly, such as except Exception: in Python or catch (Exception e) in Java, which hides bugs and makes debugging harder. Another is ignoring return codes entirely, leading to silent failures. Swallowing errors without logging or rethrowing is also common; at minimum, log the error with enough context. Overusing exceptions for control flow, like using them to exit loops, can hurt performance and readability. In asynchronous code, forgetting to handle promise rejections or callback errors causes unhandled rejections. Mixing patterns inconsistently—some functions return errors, others throw—confuses callers. Not cleaning up resources in finally or defer blocks can leak file handles or locks. Finally, exposing internal error details to users can be a security risk; sanitize messages. Each of these mistakes stems from not having a clear, consistent error handling strategy.

When to use it

Use exceptions when errors are exceptional and you want to separate error handling from normal logic, especially in languages that support them well (Python, Java, C#). Use return codes or result types when errors are expected and part of normal flow, such as parsing user input or network calls in Go or Rust. Error-first callbacks suit Node.js-style asynchronous APIs, though modern code often uses promises or async/await with try/catch. In performance-critical code, avoid exceptions for frequent, predictable failures; prefer return values. In distributed systems, consider patterns like circuit breakers and retries, which build on basic error handling. The key is to pick one primary pattern per module or layer and stick to it. Document how errors propagate and what callers should expect. When integrating with libraries, follow their conventions. There is no single best pattern; the right choice depends on language, team, and the nature of the errors you expect.

Frequently asked questions

Should I use exceptions or return codes? It depends on the language and the situation. Exceptions are idiomatic in Python, Java, and C#, and they keep normal code clean. Return codes are idiomatic in Go and C, and they make error paths explicit. Many languages support both; choose based on whether errors are exceptional or routine. How do I avoid swallowing errors? Always handle errors at the appropriate level: either recover meaningfully, log with context, or rethrow. Never catch an error and do nothing unless you have a deliberate reason. What about async errors? In JavaScript, use try/catch with async/await or .catch() on promises. In Node.js callbacks, check the first err argument. Unhandled rejections can crash the process, so always attach handlers. Finally, remember that error messages should be helpful but not leak sensitive information.