Skip to main content

Error Handling Patterns

Reference for both developers writing code and agents implementing features. Covers the full lifecycle: classification → representation → propagation → retry → observability.

Agent Trigger

Apply when: Writing try/catch blocks, defining error types, retry logic, or reviewing error paths. Rule of thumb: Fail fast, type your errors, never silently swallow; use Result types or exceptions consistently; backoff with jitter on retries.

Error Taxonomy

Understanding what kind of error you have determines how you handle it.

Expected vs. Unexpected

Expected errors belong in the return type (Result/Either or checked value). Unexpected errors surface as exceptions or panics and propagate to a global handler.

Recoverable vs. Fatal

  • Recoverable: caller can retry, fallback, or inform the user and continue
  • Fatal: system cannot safely continue; crash or escalate immediately
A crashed app is more diagnosable than an app with undefined behavior from a swallowed error.

Business Errors vs. Technical Errors

  • Business errors: domain rule violations — surface to users with actionable messages (“Insufficient balance”)
  • Technical errors: infrastructure or code failures — log internally, surface generic message to users
Never leak stack traces, internal paths, or system details to end users.

Fail-Fast Principle

Validate at the earliest possible point. Every validation deferred is a debugging cost multiplied. Rules:
  • Validate arguments before entering async or side-effecting logic
  • Raise errors immediately upon detection — not after partially completing work
  • In task-returning methods (async), throw argument exceptions synchronously before the async portion begins

Exception Best Practices

When to Use Exceptions

Use exceptions for truly exceptional conditions — events that are rare, unexpected, or represent violations of invariants. For conditions that occur routinely as part of normal flow, use conditional checks or Result types instead.

Exception Hierarchy Design

  • Derive custom exceptions from the appropriate base class, not always from the root Exception
  • Name exception classes with the Exception suffix
  • Include three constructors: (), (message), (message, innerException)
  • Add structured properties only when callers need them programmatically (not just for messages)
  • Prefer predefined exception types over custom ones where they fit precisely

What to Include in Exception Messages

  • Root cause, not a generic label (“Server error” is insufficient)
  • What went wrong, where, and ideally what the caller can do
  • Proper grammar and ending punctuation
  • No PII, no internal paths, no stack trace content in the message string itself

Never Swallow Silently


Railway-Oriented Programming / Result Types

The Result<Ok, Err> pattern (Rust, Haskell Either, Kotlin Arrow, TypeScript fp-ts) makes error handling explicit and type-safe. The compiler enforces handling — unlike exceptions, which are invisible in signatures.
Chaining / composition:

When to Use Result vs. Exceptions


Error Propagation

Let It Propagate

Default stance: if you cannot handle the error meaningfully at the current layer, let it propagate. Do not catch just to re-throw the same exception without adding context.

Catch-and-Rethrow: Add Context

When catching, wrap with additional context about what operation failed. Preserve the original as cause / innerException.

Preserve Stack Traces

In C#, use bare throw (not throw e) inside a catch block to preserve the original stack trace. When rethrowing outside the catch block, use ExceptionDispatchInfo.Capture / .Throw().

Rollback on Partial Mutation

If a method performs multiple side effects and one fails, undo completed steps. Callers must be able to assume no side effects when an exception escapes.

Retry and Backoff

What Errors Warrant Retry

Retry only transient errors: network timeouts, rate limits, temporary unavailability. Do not retry:
  • Validation errors (will always fail)
  • Authentication failures (credentials wrong, not transient)
  • Business rule violations

Exponential Backoff with Jitter

Jitter prevents thundering herd when many callers retry simultaneously.

Retry Budget

Cap total attempts and total elapsed time. See Error Budget (Agentic) for the agentic adaptation of this pattern (retry/token/runtime/session budgets). For self-healing loops: Self-Healing Loop covers the failure→retry→rollback→escalation sequence.

Error Response Design (APIs)

See API Design Patterns for full REST conventions. Summary for error responses:

Structured Error Body

HTTP Status + Error Code

HTTP status gives coarse classification. A machine-readable code string provides specific identity. Never return 200 with an error body — clients cannot distinguish success from failure without reading the body.

User-Facing vs. Developer Messages

  • message: safe to display to end users — no internals, no paths
  • details / debug: developer context, strip in production or gate behind auth
  • Never expose stack traces, SQL, or internal service names in API responses

Logging Discipline

What to Log

  • Log at ERROR: unhandled exceptions, data loss scenarios, external service failures
  • Log at WARN: degraded operation, retries, approaching limits, unexpected-but-handled states
  • Log at INFO: significant lifecycle events (server start, job complete, user auth)
  • Log at DEBUG: request details, intermediate state — only in development

Avoid Logging PII

Never log passwords, tokens, email addresses, SSNs, payment data. Mask or omit before logging.

Log Error Codes

Assign numeric or string codes to known error categories. Document them. Codes allow support teams to grep logs and correlate errors without reading prose.

Log Once at the Boundary

Log at the point where the error is handled or escalated — not at every layer it passes through. Multiple log entries for the same error create noise and inflate storage.

Anti-Patterns


Cross-References