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.
CODEBLOCK0
Language Patterns
| Language | Mechanism | Anti-Pattern |
|---|
| JavaScript | INLINECODE3 , Promise.catch, Error subclasses | INLINECODE5 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
CODEBLOCK1
Go — Error Wrapping
CODEBLOCK2
Error Boundaries
Express Error Middleware
CODEBLOCK3
React Error Boundary
CODEBLOCK4
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
CODEBLOCK5
Circuit Breaker
CODEBLOCK6
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
CODEBLOCK7
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 |
CODEBLOCK8
Logging & Monitoring
| Practice | Implementation |
|---|
| Structured logging | JSON: level, message, error, requestId, userId, INLINECODE24 |
| 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 INLINECODE30 | 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
- 1. 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 INLINECODE39 - 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
错误处理模式
交付健壮的软件。在边界处处理错误,快速且响亮地失败,绝不静默吞没异常。
错误处理哲学
| 原则 | 描述 |
|---|
| 快速失败 | 尽早检测错误——在边界处验证输入,而非深入业务逻辑 |
| 响亮失败 |
错误必须可见——记录它们、暴露它们、针对它们发出告警 |
|
在边界处处理 | 在层边界(控制器、中间件、网关)捕获并转换错误 |
|
让它崩溃 | 对于不可恢复的状态,崩溃并重启(Erlang/OTP 哲学) |
|
具体明确 | 捕获特定的错误类型,绝不使用裸 catch 或 except |
|
提供上下文 | 每个错误携带足够的上下文,无需复现即可诊断 |
错误类型
操作性错误 — 网络超时、无效的用户输入、文件未找到、数据库连接丢失。优雅处理。
程序员错误 — TypeError、空引用、断言失败。修复代码——不要捕获并压制。
javascript
// 操作性错误 — 优雅处理
try {
const data = await fetch(/api/users);
} catch (err) {
if (err.code === ECONNREFUSED) return fallbackData;
throw err; // 重新抛出意外错误
}
// 程序员错误 — 让它崩溃,修复 bug
const user = null;
user.name; // TypeError — 不要用 try/catch 捕获这个
语言模式
| 语言 | 机制 | 反模式 |
|---|
| JavaScript | try/catch、Promise.catch、Error 子类 | .catch(() => {}) 吞没错误 |
| Python |
异常、上下文管理器(with) | 裸 except: 捕获所有异常 |
|
Go | error 返回、errors.Is/As、fmt.Errorf 包装 | _ = riskyFunction() 忽略错误 |
|
Rust | Result
、Option、? 运算符 | 生产代码中使用 .unwrap() |
JavaScript — 错误子类
javascript
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, 404, { resource, id });
}
}
class ValidationError extends AppError {
constructor(errors) {
super(验证失败, VALIDATION_ERROR, 422, { errors });
}
}
Go — 错误包装
go
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(用户 %s: %w, id, ErrNotFound)
}
return nil, fmt.Errorf(查询用户 %s: %w, id, err)
}
return &user, nil
}
错误边界
Express 错误中间件
javascript
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const response = {
error: {
code: err.code || INTERNAL_ERROR,
message: err.isOperational ? err.message : 出了点问题,
...(process.env.NODE_ENV === development && { stack: err.stack }),
requestId: req.id,
},
};
logger.error(请求失败, {
err, requestId: req.id, method: req.method, path: req.path,
});
res.status(statusCode).json(response);
});
React 错误边界
tsx
import { ErrorBoundary } from react-error-boundary;
function ErrorFallback({ error, resetErrorBoundary }) {
return (
);
}
queryClient.clear()}>
重试模式
| 模式 | 何时使用 | 配置 |
|---|
| 指数退避 | 瞬时故障(网络、503) | 基础 1s,最大 30s,因子 2x |
| 退避 + 抖动 |
多个客户端重试 | 每次延迟随机 ±30% |
| 断路器 | 下游服务持续失败 | 5 次失败后断开,30s 后半开 |
| 隔板 | 隔离故障以防止级联 | 限制每个服务的并发调用数 |
| 超时 | 防止无限挂起 | 连接 5s,读取 30s,总计 60s |
带抖动的指数退避
javascript
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;
}
断路器
javascript
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(断路器已断开);
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 错误响应
| 状态码 | 名称 | 何时使用 |
|---|
| 400 | 错误请求 | 语法错误、无效 JSON |
| 401 |
未授权 | 缺少或无效的身份验证 |
| 403 | 禁止访问 | 已认证但权限不足 |
| 404 | 未找到 | 资源不存在 |
| 409 | 冲突 | 请求与当前状态冲突 |
| 422 | 不可处理的实体 | 语法正确但语义错误 |
| 429 | 请求过多 | 超出速率限制(包含 Retry-After) |
| 500 | 内部服务器错误 | 意外的服务器故障 |
| 502 | 错误网关 | 上游返回无效响应 |
| 503 | 服务不可用 | 临时过载或维护中 |
标准错误信封
json
{
error: {
code: VALIDATION_ERROR,
message: 请求体包含无效字段。,
details: [
{ field: email, message: 必须是有效的电子邮件地址 }
],
requestId: req_abc123xyz
}
}
优雅降级
| 策略 | 示例 |
|---|
| 回退值 | 当图片服务不可用时显示缓存的头像 |
| 功能开关 |
禁用不稳定的推荐引擎 |
| 缓存响应 | 使用 X-Cache: STALE 头提供过期数据 |
| 部分响应 | 返回可用数据并附带 warnings 数组 |