# Error Handling

> 设计或改进错误处理与日志时。

- Skill: `lion-1209/error-handling` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add lion-1209/error-handling`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lion-1209/error-handling/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Lion-1209 (https://skillmd.com/u/lion-1209)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lion-1209/error-handling

---


# Error Handling

## 概述

错误要**可观测**（知道发生了什么）、**可区分**（能区分类型）、**不吞**（不静默 catch）。好的错误处理让你在出问题时能快速定位，而不是对着空白或 `undefined` 发呆。

## 何时使用

- 写代码遇到边界/失败场景（空值、超时、外部调用失败），不知怎么处理
- 现有错误处理粗糙（裸 catch、吞异常），要改进
- 设计错误类型/错误码体系
- 决定"这个错该抛还是该处理、要不要重试"

**不该用**：确定性逻辑（没失败可能，别过度防御）。

## 核心内容

### 三态决策：抛 / 处理 / 重试

遇到一个可能的错误，先判断属于哪种：

- **抛（throw / propagate）**：你处理不了，或属于底层职责。往上传，让上层决策。例：数据库连不上，service 层抛，让调用方决定降级还是报错。
- **处理（handle）**：你能**在原地给出可接受的后续**——三种形态：①**恢复**（用默认值/缓存顶上，如配置缺失用默认配置）；②**降级**（核心功能照跑、非核心跳过，如推荐服务挂了就返回空列表而非整页崩溃）；③**转成业务语义**（把底层异常翻译成业务错误类型抛出，让边界层转成给用户的提示）。**业务错误（余额不足、邮箱已注册）归这里**——它们不是故障而是预期分支，不重试（结果不会变），抛一个业务错误类型即可。判断"能不能处理"的尺子：**处理后程序能否继续负责任地往下走？**能 → 处理；只是把错藏起来让上层踩坑 → 抛。
- **重试（retry）**：只对**瞬时**故障重试——网络抖动、连接超时、HTTP 429/503。指数退避、有上限，耗尽后归为"抛"。三个前提：① 有副作用的调用（支付、下单、写库）重试前必须保证幂等（幂等键或唯一约束），否则重试=重复扣款/重复写入；② **读超时**（请求已发出、响应没回来）结果可能已生效，按副作用对待，必须幂等；③ HTTP 429/503 优先遵守响应头 `Retry-After`。注意 4xx（非 429）和多数 5xx 不是瞬时错误，重试无意义，直接抛。

**原则**：越靠近错误的层越了解错误含义；但只有能负责任的层才该处理。不知道怎么办就抛上去。

### 日志

- **记什么**：上下文——输入、相关状态、错误对象（含堆栈）
- **敏感信息要脱敏后记，不要完全不记**：密码、token、身份证、银行卡——直接丢弃会丢失排查线索（"是不是这个用户的请求出问题？"）。正确做法是记**可识别但不可还原**的形式：token 记前 8 位 + `***`、手机号记后 4 位、密码只记"是否为空"不记内容。原则：能定位到"是哪个对象/哪次请求"，但不能还原出敏感值本身。
- **分级**：`debug`（排查细节）、`info`（关键业务节点）、`warn`（可疑但可继续）、`error`（出错了需关注）

**业务错误 vs 系统错误的日志级别**：邮箱已注册、余额不足这类是**预期的业务拒绝**，不算系统故障——打 `info`/`warn` 即可，别打 `error` 污染告警；DB 连不上、网络失败这类**意外故障**才打 `error`。

错误日志要让你光看日志就重现问题现场，而不是只看到 `Error: something went wrong`。

### 错误信息面向开发者

错误消息给**排查的开发者**看，不是给终端用户看：写清**发生了什么** + **可能原因** + **怎么排查**，并带上下文。

```
// 差
throw new Error("failed")

// 好
throw new Error(`用户注册失败：邮箱 ${email} 已被注册。检查是否应走登录流程。`)
```

给用户的提示要另外做（友好、不泄露技术细节），别直接把异常 message 吐给用户。

### 模式

- **自定义错误类型/错误码**：区分业务错误（邮箱已注册）和系统错误（DB 连不上），让上层针对性处理
- **fail fast**：启动期检查前置条件，不满足直接报错，别带病运行
- **边界统一兜底**：在系统边界（HTTP 中间件、API 网关）统一捕获并转换错误，别让内部异常直接漏到外部
- **别 catch 了又原样 throw**：要么加上下文再抛，要么处理掉

## 常见错误

| 问题 | 修法 |
|------|------|
| 裸 `catch(e){}` 静默吞错 | 至少记日志 + 决定抛/处理 |
| catch 了又 `throw e`（无添加） | 加上下文再抛，或处理掉 |
| 日志只记 `e.message` | 记上下文 + 完整错误对象 |
| 敏感信息要么全记要么全丢 | 脱敏后记（token 记前 8 位 + `***`），保留定位能力又不泄露 |
| 把堆栈直接吐给终端用户 | 内部日志详细，给用户友好提示 |
| 不区分错误类型 | 用自定义错误类型/错误码 |
| 无限重试/无退避 | 指数退避 + 最大次数 |

