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 again</button>
</div>
);
}
<ErrorBoundary FallbackComponent={ErrorFallback} => 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
1---2name: error-handling3description: Error handling patterns across languages and layers — operational vs programmer errors, retry strategies, circuit breakers, error boundaries, HTTP responses, graceful degradation, and structured logging. Use when designing error strategies, building resilient APIs, or reviewing error management.4---5
6# Error Handling Patterns
7
8> Ship resilient software. Handle errors at boundaries, fail fast and loud, never swallow exceptions silently.
9
10## Error Handling Philosophy
11
12| Principle | Description |
13|-----------|-------------|
14| **Fail Fast** | Detect errors early — validate inputs at the boundary, not deep in business logic |
15| **Fail Loud** | Errors must be visible — log them, surface them, alert on them |
16| **Handle at Boundaries** | Catch and translate errors at layer boundaries (controller, middleware, gateway) |
17| **Let It Crash** | For unrecoverable state, crash and restart (Erlang/OTP philosophy) |
18| **Be Specific** | Catch specific error types, never bare `catch` or `except` |
19| **Provide Context** | Every error carries enough context to diagnose without reproducing |
20
21---
22
23## Error Types
24
25**Operational errors** — network timeouts, invalid user input, file not found, DB connection lost. Handle gracefully.
26
27**Programmer errors** — `TypeError`, null dereference, assertion failures. Fix the code — don't catch and suppress.
28
29```javascript
30// Operational — handle gracefully
31try {
32 const data = await fetch('/api/users');
33} catch (err) {
34 if (err.code === 'ECONNREFUSED') return fallbackData;
35 throw err; // re-throw unexpected errors
36}
37
38// Programmer — let it crash, fix the bug
39const user = null;
40user.name; // TypeError — don't try/catch this
41```
42
43---
44
45## Language Patterns
46
47| Language | Mechanism | Anti-Pattern |
48|----------|-----------|-------------|
49| **JavaScript** | `try/catch`, `Promise.catch`, Error subclasses | `.catch(() => {})` swallowing errors |
50| **Python** | Exceptions, context managers (`with`) | Bare `except:` catching everything |
51| **Go** | `error` returns, `errors.Is/As`, `fmt.Errorf` wrapping | `_ = riskyFunction()` ignoring error |
52| **Rust** | `Result<T, E>`, `Option<T>`, `?` operator | `.unwrap()` in production code |
53
54### JavaScript — Error Subclasses
55
56```javascript
57class AppError extends Error {
58 constructor(message, code, statusCode, details = {}) {
59 super(message);
60 this.name = this.constructor.name;
61 this.code = code;
62 this.statusCode = statusCode;
63 this.details = details;
64 this.isOperational = true;
65 }
66}
67
68class NotFoundError extends AppError {
69 constructor(resource, id) {
70 super(`${resource} not found`, 'NOT_FOUND', 404, { resource, id });
71 }
72}
73
74class ValidationError extends AppError {
75 constructor(errors) {
76 super('Validation failed', 'VALIDATION_ERROR', 422, { errors });
77 }
78}
79```
80
81### Go — Error Wrapping
82
83```go
84func GetUser(id string) (*User, error) {
85 row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
86 var user User
87 if err := row.Scan(&user.ID, &user.Name); err != nil {
88 if errors.Is(err, sql.ErrNoRows) {
89 return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
90 }
91 return nil, fmt.Errorf("querying user %s: %w", id, err)
92 }
93 return &user, nil
94}
95```
96
97---
98
99## Error Boundaries
100
101### Express Error Middleware
102
103```javascript
104app.use((err, req, res, next) => {
105 const statusCode = err.statusCode || 500;
106 const response = {
107 error: {
108 code: err.code || 'INTERNAL_ERROR',
109 message: err.isOperational ? err.message : 'Something went wrong',
110 ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
111 requestId: req.id,
112 },
113 };
114
115 logger.error('Request failed', {
116 err, requestId: req.id, method: req.method, path: req.path,
117 });
118
119 res.status(statusCode).json(response);
120});
121```
122
123### React Error Boundary
124
125```tsx
126import { ErrorBoundary } from 'react-error-boundary';
127
128function ErrorFallback({ error, resetErrorBoundary }) {
129 return (
130 <div role="alert">
131 <h2>Something went wrong</h2>
132 <pre>{error.message}</pre>
133 <button onClick={resetErrorBoundary}>Try again</button>
134 </div>
135 );
136}
137
138<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => queryClient.clear()}>
139 <App />
140</ErrorBoundary>
141```
142
143---
144
145## Retry Patterns
146
147| Pattern | When to Use | Config |
148|---------|-------------|--------|
149| **Exponential Backoff** | Transient failures (network, 503) | Base 1s, max 30s, factor 2x |
150| **Backoff + Jitter** | Multiple clients retrying | Random ±30% on each delay |
151| **Circuit Breaker** | Downstream service failing repeatedly | Open after 5 failures, half-open after 30s |
152| **Bulkhead** | Isolate failures to prevent cascade | Limit concurrent calls per service |
153| **Timeout** | Prevent indefinite hangs | Connect 5s, read 30s, total 60s |
154
155### Exponential Backoff with Jitter
156
157```javascript
158async function withRetry(fn, { maxRetries = 3, baseDelay = 1000, maxDelay = 30000 } = {}) {
159 for (let attempt = 0; attempt <= maxRetries; attempt++) {
160 try {
161 return await fn();
162 } catch (err) {
163 if (attempt === maxRetries || !isRetryable(err)) throw err;
164 const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
165 const jitter = delay * (0.7 + Math.random() * 0.6);
166 await new Promise((r) => setTimeout(r, jitter));
167 }
168 }
169}
170
171function isRetryable(err) {
172 return [408, 429, 500, 502, 503, 504].includes(err.statusCode) || err.code === 'ECONNRESET';
173}
174```
175
176### Circuit Breaker
177
178```javascript
179class CircuitBreaker {
180 constructor({ threshold = 5, resetTimeout = 30000 } = {}) {
181 this.state = 'CLOSED'; // CLOSED → OPEN → HALF_OPEN → CLOSED
182 this.failureCount = 0;
183 this.threshold = threshold;
184 this.resetTimeout = resetTimeout;
185 this.nextAttempt = 0;
186 }
187
188 async call(fn) {
189 if (this.state === 'OPEN') {
190 if (Date.now() < this.nextAttempt) throw new Error('Circuit is OPEN');
191 this.state = 'HALF_OPEN';
192 }
193 try {
194 const result = await fn();
195 this.onSuccess();
196 return result;
197 } catch (err) {
198 this.onFailure();
199 throw err;
200 }
201 }
202
203 onSuccess() { this.failureCount = 0; this.state = 'CLOSED'; }
204 onFailure() {
205 this.failureCount++;
206 if (this.failureCount >= this.threshold) {
207 this.state = 'OPEN';
208 this.nextAttempt = Date.now() + this.resetTimeout;
209 }
210 }
211}
212```
213
214---
215
216## HTTP Error Responses
217
218| Status | Name | When to Use |
219|--------|------|-------------|
220| **400** | Bad Request | Malformed syntax, invalid JSON |
221| **401** | Unauthorized | Missing or invalid authentication |
222| **403** | Forbidden | Authenticated but insufficient permissions |
223| **404** | Not Found | Resource does not exist |
224| **409** | Conflict | Request conflicts with current state |
225| **422** | Unprocessable Entity | Valid syntax but semantic errors |
226| **429** | Too Many Requests | Rate limit exceeded (include `Retry-After`) |
227| **500** | Internal Server Error | Unexpected server failure |
228| **502** | Bad Gateway | Upstream returned invalid response |
229| **503** | Service Unavailable | Temporarily overloaded or maintenance |
230
231### Standard Error Envelope
232
233```json
234{
235 "error": {
236 "code": "VALIDATION_ERROR",
237 "message": "The request body contains invalid fields.",
238 "details": [
239 { "field": "email", "message": "Must be a valid email address" }
240 ],
241 "requestId": "req_abc123xyz"
242 }
243}
244```
245
246---
247
248## Graceful Degradation
249
250| Strategy | Example |
251|----------|---------|
252| **Fallback values** | Show cached avatar when image service is down |
253| **Feature flags** | Disable unstable recommendation engine |
254| **Cached responses** | Serve stale data with `X-Cache: STALE` header |
255| **Partial response** | Return available data with `warnings` array |
256
257```javascript
258async function getProductPage(productId) {
259 const product = await productService.get(productId); // critical — propagate errors
260
261 const [reviews, recommendations] = await Promise.allSettled([
262 reviewService.getForProduct(productId),
263 recommendationService.getForProduct(productId),
264 ]);
265
266 return {
267 product,
268 reviews: reviews.status === 'fulfilled' ? reviews.value : [],
269 recommendations: recommendations.status === 'fulfilled' ? recommendations.value : [],
270 warnings: [reviews, recommendations]
271 .filter((r) => r.status === 'rejected')
272 .map((r) => ({ service: 'degraded', reason: r.reason.message })),
273 };
274}
275```
276
277---
278
279## Logging & Monitoring
280
281| Practice | Implementation |
282|----------|---------------|
283| **Structured logging** | JSON: `level`, `message`, `error`, `requestId`, `userId`, `timestamp` |
284| **Error tracking** | Sentry, Datadog, Bugsnag — automatic capture with source maps |
285| **Alert thresholds** | Error rate > 1%, P99 latency > 2s, 5xx spike |
286| **Correlation IDs** | Pass `requestId` through all service calls |
287| **Log levels** | `error` = needs attention, `warn` = degraded, `info` = normal, `debug` = dev |
288
289---
290
291## Anti-Patterns
292
293| Anti-Pattern | Fix |
294|-------------|-----|
295| **Swallowing errors** `catch (e) {}` | Log and re-throw, or handle explicitly |
296| **Generic catch-all** at every level | Catch specific types, let unexpected errors bubble |
297| **Error as control flow** | Use conditionals, return values, or option types |
298| **Stringly-typed errors** `throw "wrong"` | Throw `Error` objects with codes and context |
299| **Logging and throwing** | Log at the boundary only, or wrap and re-throw |
300| **Catch-and-return-null** | Return `Result` type, throw, or return error object |
301| **Ignoring Promise rejections** | Always `await` or attach `.catch()` |
302| **Exposing internals** | Sanitize responses; log details server-side only |
303
304---
305
306## NEVER Do
307
3081. **NEVER swallow errors silently** — `catch (e) {}` hides bugs and causes silent data corruption
3092. **NEVER expose stack traces, SQL errors, or file paths in API responses** — log details server-side only
3103. **NEVER use string throws** — `throw 'error'` has no stack trace, no type, no context
3114. **NEVER catch and return null without explanation** — callers have no idea why the operation failed
3125. **NEVER ignore unhandled Promise rejections** — always `await` or attach `.catch()`
3136. **NEVER cache error responses** — 5xx and transient errors must not be cached and re-served
3147. **NEVER use exceptions for normal control flow** — exceptions are for exceptional conditions
3158. **NEVER return generic "Something went wrong" without logging the real error** — always log the full error server-side with request context