Error Handling Patterns
Ship resilient software. Handle errors at boundaries, fail fast and loud, never swallow exceptions silently.
Error Handling Philosophy
| Principle | Description |
|---|
| Fail Fast | Detect errors early — validate inputs at the boundary, not deep in business logic |
| Fail Loud | Errors must be visible — log them, surface them, alert on them |
| Handle at Boundaries | Catch and translate errors at layer boundaries (controller, middleware, gateway) |
| Let It Crash | For unrecoverable state, crash and restart (Erlang/OTP philosophy) |
| Be Specific | Catch specific error types, never bare catch or except |
| Provide Context | Every error carries enough context to diagnose without reproducing |
Error Types
Operational errors — network timeouts, invalid user input, file not found, DB connection lost. Handle gracefully.
Programmer errors — TypeError, null dereference, assertion failures. Fix the code — don't catch and suppress.
// Operational — handle gracefully
try {
const data = await fetch('/api/users');
} catch (err) {
if (err.code === 'ECONNREFUSED') return fallbackData;
throw err; // re-throw unexpected errors
}
// Programmer — let it crash, fix the bug
const user = null;
user.name; // TypeError — don't try/catch this
Language Patterns
| Language | Mechanism | Anti-Pattern |
|---|
| JavaScript | try/catch, Promise.catch, Error subclasses | .catch(() => {}) swallowing errors |
| Python | Exceptions, context managers (with) | Bare except: catching everything |
| Go | error returns, errors.Is/As, fmt.Errorf wrapping | _ = riskyFunction() ignoring error |
| Rust | Result<T, E>, Option<T>, ? operator | .unwrap() in production code |
JavaScript — Error Subclasses
class AppError extends Error {
constructor(message, code, statusCode, details = {}) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.statusCode = statusCode;
this.details = details;
this.isOperational = true;
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} not found`, 'NOT_FOUND', 404, { resource, id });
}
}
class ValidationError extends AppError {
constructor(errors) {
super('Validation failed', 'VALIDATION_ERROR', 422, { errors });
}
}
Go — Error Wrapping
func GetUser(id string) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
var user User
if err := row.Scan(&user.ID, &user.Name); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("querying user %s: %w", id, err)
}
return &user, nil
}
Error Boundaries
Express Error Middleware
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const response = {
error: {
code: err.code || 'INTERNAL_ERROR',
message: err.isOperational ? err.message : 'Something went wrong',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
requestId: req.id,
},
};
logger.error('Request failed', {
err, requestId: req.id, method: req.method, path: req.path,
});
res.status(statusCode).json(response);
});
React Error Boundary
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => queryClient.clear()}>
<App />
</ErrorBoundary>
Retry Patterns
| Pattern | When to Use | Config |
|---|
| Exponential Backoff | Transient failures (network, 503) | Base 1s, max 30s, factor 2x |
| Backoff + Jitter | Multiple clients retrying | Random ±30% on each delay |
| Circuit Breaker | Downstream service failing repeatedly | Open after 5 failures, half-open after 30s |
| Bulkhead | Isolate failures to prevent cascade | Limit concurrent calls per service |
| Timeout | Prevent indefinite hangs | Connect 5s, read 30s, total 60s |
Exponential Backoff with Jitter
async function withRetry(fn, { maxRetries = 3, baseDelay = 1000, maxDelay = 30000 } = {}) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries || !isRetryable(err)) throw err;
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
const jitter = delay * (0.7 + Math.random() * 0.6);
await new Promise((r) => setTimeout(r, jitter));
}
}
}
function isRetryable(err) {
return [408, 429, 500, 502, 503, 504].includes(err.statusCode) || err.code === 'ECONNRESET';
}
Circuit Breaker
class CircuitBreaker {
constructor({ threshold = 5, resetTimeout = 30000 } = {}) {
this.state = 'CLOSED'; // CLOSED → OPEN → HALF_OPEN → CLOSED
this.failureCount = 0;
this.threshold = threshold;
this.resetTimeout = resetTimeout;
this.nextAttempt = 0;
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) throw new Error('Circuit is OPEN');
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
onSuccess() { this.failureCount = 0; this.state = 'CLOSED'; }
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
}
HTTP Error Responses
| Status | Name | When to Use |
|---|
| 400 | Bad Request | Malformed syntax, invalid JSON |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but insufficient permissions |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Request conflicts with current state |
| 422 | Unprocessable Entity | Valid syntax but semantic errors |
| 429 | Too Many Requests | Rate limit exceeded (include Retry-After) |
| 500 | Internal Server Error | Unexpected server failure |
| 502 | Bad Gateway | Upstream returned invalid response |
| 503 | Service Unavailable | Temporarily overloaded or maintenance |
Standard Error Envelope
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request body contains invalid fields.",
"details": [
{ "field": "email", "message": "Must be a valid email address" }
],
"requestId": "req_abc123xyz"
}
}
Graceful Degradation
| Strategy | Example |
|---|
| Fallback values | Show cached avatar when image service is down |
| Feature flags | Disable unstable recommendation engine |
| Cached responses | Serve stale data with X-Cache: STALE header |
| Partial response | Return available data with warnings array |
async function getProductPage(productId) {
const product = await productService.get(productId); // critical — propagate errors
const [reviews, recommendations] = await Promise.allSettled([
reviewService.getForProduct(productId),
recommendationService.getForProduct(productId),
]);
return {
product,
reviews: reviews.status === 'fulfilled' ? reviews.value : [],
recommendations: recommendations.status === 'fulfilled' ? recommendations.value : [],
warnings: [reviews, recommendations]
.filter((r) => r.status === 'rejected')
.map((r) => ({ service: 'degraded', reason: r.reason.message })),
};
}
Logging & Monitoring
| Practice | Implementation |
|---|
| Structured logging | JSON: level, message, error, requestId, userId, timestamp |
| Error tracking | Sentry, Datadog, Bugsnag — automatic capture with source maps |
| Alert thresholds | Error rate > 1%, P99 latency > 2s, 5xx spike |
| Correlation IDs | Pass requestId through all service calls |
| Log levels | error = needs attention, warn = degraded, info = normal, debug = dev |
Anti-Patterns
| Anti-Pattern | Fix |
|---|
Swallowing errors catch (e) {} | Log and re-throw, or handle explicitly |
| Generic catch-all at every level | Catch specific types, let unexpected errors bubble |
| Error as control flow | Use conditionals, return values, or option types |
Stringly-typed errors throw "wrong" | Throw Error objects with codes and context |
| Logging and throwing | Log at the boundary only, or wrap and re-throw |
| Catch-and-return-null | Return Result type, throw, or return error object |
| Ignoring Promise rejections | Always await or attach .catch() |
| Exposing internals | Sanitize responses; log details server-side only |
NEVER Do
- NEVER swallow errors silently —
catch (e) {} hides bugs and causes silent data corruption
- NEVER expose stack traces, SQL errors, or file paths in API responses — log details server-side only
- NEVER use string throws —
throw 'error' has no stack trace, no type, no context
- NEVER catch and return null without explanation — callers have no idea why the operation failed
- NEVER ignore unhandled Promise rejections — always
await or attach .catch()
- NEVER cache error responses — 5xx and transient errors must not be cached and re-served
- NEVER use exceptions for normal control flow — exceptions are for exceptional conditions
- NEVER return generic "Something went wrong" without logging the real error — always log the full error server-side with request context