Error Handling

Purpose

Errors are an inevitable part of software operation.

How a system handles errors determines its reliability, debuggability and operational maturity. Good error handling transforms failures into recoverable events. Poor error handling turns minor issues into production incidents.

This guide defines the principles and patterns for handling errors in a way that is consistent, predictable and maintainable.

Background

Error handling is often treated as an afterthought — added reactively after a production failure reveals a gap.

Proactive error handling treats errors as a design concern from the start. Every function, service and system boundary should define:

Principles

Distinguish Recoverable from Unrecoverable Errors

Errors fall into two categories:

CategoryDefinitionExamples
RecoverableThe system can continue operatingValidation failure,
despite the error.network timeout,
resource not found
UnrecoverableThe system cannot safely continue.Null pointer dereference,
Fail fast and report.out of memory,
configuration error

Recoverable errors should be handled gracefully.

Unrecoverable errors should be surfaced immediately with full context.

Never Silently Swallow Errors

An error that is caught and discarded is worse than an error that propagates.

Silently swallowed errors:

If an error cannot be handled meaningfully at the current level, let it propagate to a level that can handle it.

Handle Errors at the Appropriate Level

Errors should be handled at the level of abstraction that has enough context to make a decision about recovery.

A low-level database driver should not decide how to handle a constraint violation. It should raise the error and let the business logic layer decide.

Provide Context with Every Error

Errors should include enough information to diagnose the problem without requiring reproduction.

Good error messages include:

Good:

UserRegistrationFailed: Could not register user
  (email=john@example.com). Reason: Email already taken.
  Request ID: req-abc-123

Avoid:

Error: Validation failed

Fail Fast

Detect and report errors as early as possible.

Validating inputs at the boundary prevents corrupted state from propagating deeper into the system. Validating preconditions before execution prevents wasted work.

Patterns

Return Types vs Exceptions

Choose the error mechanism that matches the context:

MechanismUse When
Return typesThe error is expected and callers should handle it
(e.g., validation, not found).
ExceptionsThe error is unexpected or cannot be handled at the
call site (e.g., network failure, configuration error).
Result typesYou want type-safe error handling without exceptions
(e.g., Result[T, E], Either, =Ok=).

Prefer return types or result types for expected errors. Reserve exceptions for exceptional conditions.

Error Boundaries

Define explicit boundaries where errors are caught, logged, and transformed.

Common boundaries include:

Retry with Backoff

Transient errors (network timeouts, database deadlocks) should be retried with exponential backoff.

Retry guidelines:

Circuit Breaker

For errors that indicate a downstream system is unavailable, use a circuit breaker to fail fast rather than wasting resources on repeated failed calls.

The circuit breaker trips after a threshold of failures, then allows periodic test requests to determine if the downstream system has recovered.

Logging

Log Levels

LevelUse When
ERRORA failure that requires human investigation.
WARNAn unexpected condition that the system handled.
INFOSignificant lifecycle events (startup, shutdown, deploy).
DEBUGDetailed information for diagnosing issues.
TRACEVery detailed diagnostic information.

Structured Logging

Always use structured logging (JSON or key-value format) rather than free-text messages.

Good:

{"event": "user_registration_failed",
 "user_id": "abc-123",
 "reason": "email_taken",
 "duration_ms": 45,
 "request_id": "req-xyz-789"}

Structured logs are searchable, filterable and machine-parseable.

Anti-patterns

Catching and Ignoring

try:
    risky_operation()
except Exception:
    pass  # Never do this.

If you must catch an exception you cannot handle, log it with context before continuing.

Catching Too Broadly

Catching Exception or RuntimeError hides unexpected bugs that should not be recovered from.

Catch specific exception types whenever possible.

Error Codes Without Context

Returning a numeric error code without explanation forces engineers to cross-reference documentation to understand the failure.

Use descriptive error messages and structured data instead.

AI Integration

AI assistants can help with error handling by:

When asking AI for error handling advice, provide:

Related Documents