Error Handling Patterns
Robust error handling is the difference between an app that degrades gracefully and one that leaks stack traces or dies silently. Good handling improves reliability, makes incidents debuggable, and protects users from confusing failures.
The core idea: decide what's expected (and handle it) versus what's a bug (and fail loudly), then make every failure produce a consistent response and a useful log entry.
TL;DR
- Classify errors: operational (expected, often retryable) vs programmer (bugs).
- Centralize handling in one place; return a consistent error shape.
- Show users a safe message; log the technical detail with a request ID.
- Never swallow errors, and never leak internals to the client.
Quick Example
A central handler that logs the detail and returns a safe, consistent response:
Core Concepts
Classify your errors
The key split is operational vs programmer: operational errors are part of normal life and deserve handling; programmer errors are bugs you shouldn't paper over.
Design the error response
Use one schema everywhere, map errors to the right HTTP status, include a stable machine-readable code, and attach a request_id so logs and support tickets line up. See REST API Design.
Best Practices
Handle centrally, throw locally
Let code throw typed errors close to the problem, and convert them to responses in one middleware/handler. This keeps individual handlers clean and the output consistent.
Log with context, respond safely
Retry only what's safe
Retry idempotent, transient failures (network blips, 503s) with exponential backoff and jitter. Never blindly retry non-idempotent writes — see Message Queues for at-least-once patterns.
Common Mistakes
Swallowing errors
Leaking internals to the client
FAQ
Exceptions or result types?
Both work. Exceptions suit "exceptional" failures and centralized handling; result types (Result<T, E>) make expected errors explicit at the call site with no hidden control flow. Many codebases throw for programmer errors and return results for expected ones.
What should I return to the client?
A consistent envelope with a stable code, a human-readable message (safe for 4xx, generic for 5xx), and a request_id. Never raw stack traces or database errors.
Which errors should I retry?
Only idempotent, transient ones — timeouts, connection resets, 429/503. Use exponential backoff with jitter, and cap the attempts. Retrying a non-idempotent write can double-charge or duplicate data.
Where should I handle errors?
Throw near the cause with enough context, and convert to responses in a single central handler. Avoid scattering try/catch that each format errors differently.
Related Topics
- Logging — Capturing the detail behind an error
- Monitoring — Error rates and alerting
- REST API Design — Status codes and error shapes
- Message Queues — Retries and dead-letter queues