name: error-handling description: Error handling patterns for consistent, meaningful error management across the stack. Use when implementing any feature that can fail.
Error Handling
Principles
- Errors are expected, not exceptional. Network calls fail. Users provide invalid input. Disks fill up. Treat failures as a normal part of the flow.
- Handle errors at the right level. The layer that has enough context to make a decision handles the error. Lower layers propagate with context; they do not swallow or silently ignore.
- Provide context. An error message of "failed" is useless. Include what operation failed, what the inputs were, and what the caller should do about it.
- Don't swallow errors. An empty
catchblock that continues execution as if nothing happened is a bug waiting to surface in production.
At System Boundaries
System boundaries are where external data enters your system: HTTP requests, message queue consumers, CLI arguments, file uploads.
- Validate all input before it touches business logic. Check type, format, length, and allowed values.
- Return structured error responses. Clients need machine-readable error codes, not just human-readable messages.
{ "error": "VALIDATION_FAILED", "field": "email", "message": "must be a valid email address" } - Never expose internal details in error responses (stack traces, SQL errors, internal paths, system versions).
- Log the full error internally. The sanitized response goes to the client; the full context (stack trace, request ID, inputs) goes to your logging system.
In Business Logic
Business logic is the core of your application. Errors here are domain events, not infrastructure surprises.
- Use typed or custom errors. A generic
Errortells the caller nothing.InsufficientFundsError,OrderAlreadyShippedError, andUserNotFoundErrorconvey meaning and can be handled selectively. - Propagate with context. When rethrowing, wrap the original error:
throw new PaymentProcessingError("failed to charge card", { cause: originalError, orderId }) - Fail fast. Validate preconditions at the start of a function. Don't let invalid state propagate deep into logic only to fail mysteriously later.
- Make impossible states unrepresentable. Use the type system to eliminate invalid combinations at compile time rather than checking for them at runtime.
In Infrastructure Code
Infrastructure code talks to databases, external APIs, message queues, and file systems. These components fail transiently.
- Retry transient failures with exponential backoff. Network blips, rate limits, and momentary unavailability are retryable. Authorization errors and validation errors are not.
- Use circuit breakers. When a downstream service is consistently failing, stop hammering it. Open the circuit, return a fast failure, and allow time to recover.
- Set timeouts on all external calls. A call without a timeout can hang forever. Every network call, database query, and external service call needs an explicit timeout.
- Degrade gracefully. When a non-critical dependency fails (a recommendations service, an analytics call), the main flow should continue with reduced functionality rather than failing entirely.
Anti-Patterns
Avoid these patterns — they make bugs harder to find and production incidents harder to diagnose.
| Anti-pattern | Why it's harmful |
|---|---|
| Empty catch block | Silently discards the error. The program continues in an invalid state. |
| Catch-and-rethrow without context | Loses the original error message and stack trace. |
Returning null or undefined for failure | Forces every caller to remember to check, and they often won't. |
| Log-and-throw | Produces duplicate log entries and confusing stack traces. Log or throw, not both. |
| Generic error messages | "Something went wrong" is not actionable for users or on-call engineers. |
Catching Error to ignore specific errors | Catches too broadly; suppresses unexpected errors alongside the expected one. |