关于 error-handling-patterns
面向软件工程的错误处理技能,指导在 TypeScript、Python 和 React 中选择并应用策略。内容按错误语义区分处理方式,涵盖类型化错误、重试、恢复和诊断信息。
主要功能
- 01比较异常、Result 类型与错误码
- 02区分暂时性和永久性错误
- 03设计类型化错误层级
- 04应用重试、退避和熔断器
- 05处理 React 渲染错误
- 06保留 Python 异常链
适用场景
- 01为库或 CLI 选择错误模型
- 02为暂时性故障设计重试
- 03添加路由级 React 错误边界
- 04在 Python 重新抛出异常时保留上下文
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill error-handling-patterns安装后在 Claude Code 中输入/error-handling-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.claude-plugin/plugin.jsonreferences/retry-patterns.mderror-hierarchy-examples.md详细介绍
错误处理模式
设计错误处理策略,让失败显式、可恢复且易于调试。核心技能是让错误处理风格与错误语义相匹配:并非所有错误都相同,一视同仁地处理它们,会导致系统对所有错误的处理效果都一样糟糕。
适用场景
✅ 适用于:
- 为某个领域选择异常、Result 类型或错误代码
- 在 TypeScript 或 Python 中设计类型化错误层次结构
- 实现带退避、抖动和熔断机制的重试逻辑
- 构建 React 错误边界和优雅降级机制
- 为用户和开发者组织错误信息
- Python 异常链以及
__cause__/__context__语义
❌ 不适用于:
- 调试特定的运行时错误(使用调试器或领域技能)
- 日志管道基础设施(使用可观测性技能)
- APM/监控配置(使用 site-reliability-engineer 技能)
- 为错误路径编写测试(使用 vitest-testing-patterns 技能)
核心决策:异常、Result 类型还是错误代码
flowchart TD
Q1{Is this a programming error\nor contract violation?} -->|Yes| EX[Throw exception\nlet it crash]
Q1 -->|No| Q2{Is the error part of\nnormal control flow?}
Q2 -->|Yes| Q3{What is the call site context?}
Q2 -->|No| Q4{Do callers need to\ndistinguish error types?}
Q3 -->|Functional / monad-friendly| RT[Result or Either type]
Q3 -->|Simple script or CLI| EC[Error code + message]
Q4 -->|Yes| EH[Typed exception hierarchy]
Q4 -->|No| GE[Generic exception\nwith structured message]
EX --> NOTE1[Never catch at boundary —\nlet process restart]
RT --> NOTE2[Compose with map/flatMap;\ncheck references/error-hierarchy-examples.md]
EH --> NOTE3[See hierarchy design rules below]
经验法则:
- 库代码:优先使用 Result 类型,绝不要强迫调用方处理你的异常
- 应用代码:类型化异常层次结构效果很好;错误属于异常情况
- CLI/脚本:错误代码即可;用户就是错误边界
- 异步工作器:使用 Result 类型或带有重试元数据的结构化错误对象
错误分类
在决定如何处理之前,先从两个维度对每个错误进行分类:
| 暂时性(重试可能成功) | 永久性(重试无济于事) | |
|---|---|---|
| 用户可处理 | 速率限制、超出配额 | 输入无效、未授权 |
| 系统可处理 | 网络超时、数据库连接 | 数据损坏、架构不匹配 |
此分类决定:
- 是否重试(仅限暂时性错误)
- 向用户显示什么(用户可处理 → 提示信息;系统可处理 → 通用错误 + 跟踪 ID)
- 是否通知值班人员(系统永久性错误 → 紧急通知;暂时性错误激增 → 告警)
是否应该重试此错误?
flowchart TD
E[Error occurs] --> C1{Is error transient?\nTimeout, 429, 503, connection reset}
C1 -->|No| FAIL[Fail immediately\nReturn error to caller]
C1 -->|Yes| C2{Have we exceeded\nmax retry attempts?}
C2 -->|Yes| DLQ[Send to dead letter queue\nor return final failure]
C2 -->|No| C3{Is circuit breaker OPEN?}
C3 -->|Yes| CB[Return circuit-open error\nDo not attempt request]
C3 -->|No| WAIT[Wait: exponential backoff\n+ full jitter]
WAIT --> RETRY[Retry request]
RETRY --> C1
CB --> PROBE{After timeout:\nsend probe request}
PROBE -->|Success| CLOSE[Close circuit\nResume normal traffic]
PROBE -->|Fail| CB
有关退避公式、抖动策略和熔断器实现,请参阅 references/retry-patterns.md。
TypeScript:错误层次结构设计
// Base application error — all domain errors extend this
class AppError extends Error {
readonly code: string;
readonly statusCode: number;
readonly isOperational: boolean; // false = programmer error, crash process
constructor(message: string, code: string, statusCode: number, isOperational = true) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
// Domain-specific errors
class ValidationError extends AppError {
readonly fields: Record<string, string[]>;
constructor(fields: Record<string, string[]>) {
super('Validation failed', 'VALIDATION_ERROR', 422);
this.fields = fields;
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} ${id} not found`, 'NOT_FOUND', 404);
}
}
class RateLimitError extends AppError {
readonly retryAfterMs: number;
constructor(retryAfterMs: number) {
super('Rate limit exceeded', 'RATE_LIMIT', 429);
this.retryAfterMs = retryAfterMs;
}
}
有关 Python 对应方案、Result 类型实现和完整的层次结构模式,请参阅 references/error-hierarchy-examples.md。
Result 类型模式(TypeScript)
当错误是操作的预期结果(解析、API 调用、数据库查询)时,应使用 Result,而不是抛出异常:
type Result<T, E = AppError> =
| { ok: true; value: T }
| { ok: false; error: E };
// Helpers
const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });
// Usage — caller is forced to handle both cases
async function fetchUser(id: string): Promise<Result<User, NotFoundError | NetworkError>> {
try {
const user = await db.users.findById(id);
if (!user) return err(new NotFoundError('User', id));
return ok(user);
} catch (e) {
return err(new NetworkError('DB unavailable', { cause: e }));
}
}
// At call site — no silent failures
const result = await fetchUser(userId);
if (!result.ok) {
if (result.error instanceof NotFoundError) return res.status(404).json(...);
return res.status(500).json(...);
}
const user = result.value; // typed, safe
React 错误边界
错误边界会捕获渲染期间发生的异常,但不会捕获异步错误(获取失败、setTimeout、事件处理程序)。
class RouteErrorBoundary extends React.Component<Props, State> {
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
// Log to error tracking, not console.error in production
logger.error('Render error', { error, componentStack: info.componentStack });
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} onRetry={this.reset} />;
}
return this.props.children;
}
}
在路由级别(每个页面一个)以及独立且开销较大的子树周围(图表、富文本编辑器)设置边界。不要为每个组件都添加边界——粒度过细会削弱其优势。
Python:异常链
Python 的 raise X from Y 语法会保留因果链;重新抛出时务必使用它:
class AppError(Exception):
"""Base error. All domain errors subclass this."""
def __init__(self, message: str, code: str, status: int = 500):
super().__init__(message)
self.code = code
self.status = status
class DatabaseError(AppError):
def __init__(self, operation: str, cause: Exception):
super().__init__(f"DB error during {operation}", "DB_ERROR", 503)
self.__cause__ = cause # explicit chain
# In application code
try:
result = db.execute(query)
except psycopg2.OperationalError as e:
raise DatabaseError("user_fetch", e) from e # preserves full traceback
结构化错误日志
记录错误时提供足够的上下文,以便无需阅读代码即可诊断:
// Good: structured, queryable, developer-oriented
logger.error('Payment processing failed', {
error: {
code: error.code,
message: error.message,
stack: error.stack,
},
context: {
userId,
orderId,
amount,
paymentProvider,
attempt: retryCount,
},
correlation: { requestId, traceId },
});
// Then surface a sanitized message to the user
// NEVER leak error.message to users — it may contain internals
return res.status(500).json({
error: 'Payment could not be processed. Please try again.',
errorId: requestId, // so support can look it up
});
反模式
反模式:宝可梦式异常处理
新手:“把所有内容都包在 try/catch 里并记录错误。至少程序不会崩溃。”
专家:无条件捕获所有异常(“统统抓起来”)会掩盖程序员错误、隐藏资源泄漏,并将明显的失败转变为无声的数据损坏。数据正在被悄悄丢弃,系统却看起来一切正常。
// Wrong — swallows everything including programming errors
try {
await processOrder(order);
} catch (e) {
console.error('something went wrong', e); // lost forever
}
// Right — catch only what you can handle, let the rest propagate
try {
await processOrder(order);
} catch (e) {
if (e instanceof RateLimitError) {
await queue.requeue(order, { delay: e.retryAfterMs });
return;
}
// programming errors, unexpected DB errors — let them crash
throw e;
}
检测方式:catch (e) { }、没有重新抛出的 catch (e) { log(e) }、Python 中的 except Exception as e: pass。任何没有条件判断且不重新抛出的 catch 块。
时间线:这种做法一直都是错的。随着 async/await 时代的到来(2017 年起),它又变得尤为紧迫,因为被吞掉的 Promise 拒绝比被吞掉的同步异常更难发现。
反模式:字符串类型错误
新手:“我会把错误类型放在消息字符串里:throw new Error('NOT_FOUND: User 123')”
专家:基于字符串的错误类型会迫使调用方解析字符串,在重构时容易失效,无法提供 IDE 支持,也让穷举匹配变得不可能。调用方会对字符串进行模式匹配,但这些字符串会随着代码库的演进而发生变化。
// Wrong — caller must parse strings, breaks silently on rename
throw new Error(`RATE_LIMIT: retry after ${ms}ms`);
// Caller: if (error.message.startsWith('RATE_LIMIT')) { ... }
// Right — typed, refactor-safe, IDE-navigable
throw new RateLimitError(ms);
// Caller: if (error instanceof RateLimitError) { ... error.retryAfterMs ... }
Python 等效写法:
# Wrong
raise Exception(f"rate_limit:{retry_after}")
# Right
raise RateLimitError(retry_after_ms=retry_after)
LLM 常见错误:在 StackOverflow 示例上训练的大模型经常会生成字符串类型错误,因为 StackOverflow 的回答优先追求简洁,而非正确性。在教程中,用字符串表示错误代码看起来很简洁。
检测方法:到处都在检查 instanceof Error;在 catch 块中使用字符串 .startsWith() 或 .includes();错误代码存储在 message 字段中,而不是专用属性中。
参考资料
references/retry-patterns.md— 实现重试逻辑时可查阅:指数退避公式、全抖动与等抖动、熔断器状态机、死信队列references/error-hierarchy-examples.md— 可查阅完整的 TypeScript 和 Python 类型化错误类示例、Result monad 实现以及错误边界模式