mcpField
E
error-handling-patterns

error-handling-patterns

github.com/curiositech开发263132 次安装2 天前更新
代码仓库

关于 error-handling-patterns

面向软件工程的错误处理技能,指导在 TypeScript、Python 和 React 中选择并应用策略。内容按错误语义区分处理方式,涵盖类型化错误、重试、恢复和诊断信息。

主要功能

  1. 01比较异常、Result 类型与错误码
  2. 02区分暂时性和永久性错误
  3. 03设计类型化错误层级
  4. 04应用重试、退避和熔断器
  5. 05处理 React 渲染错误
  6. 06保留 Python 异常链

适用场景

  1. 01为库或 CLI 选择错误模型
  2. 02为暂时性故障设计重试
  3. 03添加路由级 React 错误边界
  4. 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.json
references/
retry-patterns.md
error-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 实现以及错误边界模式

评论

支持 Markdown