| name | error-handling |
| description | 设计或改进错误处理与日志时。 |
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 位 + ***),保留定位能力又不泄露 |
| 把堆栈直接吐给终端用户 | 内部日志详细,给用户友好提示 |
| 不区分错误类型 | 用自定义错误类型/错误码 |
| 无限重试/无退避 | 指数退避 + 最大次数 |