3.5 dsh-error-handling:错误处理原则

它是什么

dsh-error-handling 的官方描述:在设计或评审失败路径、错误边界与恢复策略时,应用错误处理原则。

约 2389 字节,是这批技能里最短的几个之一。短不代表不重要——错误处理恰恰是最容易被一句话带过、最后在线上爆炸的部分。反过来看,能把原则压缩到两千多字节还覆盖五个维度,说明每句都是干货,没有一句废话。

描述里的三个场景也值得注意:设计(动手前想清楚)、评审(看别人代码时照着查)、恢复策略(坏了怎么收拾)。它贯穿的是失败路径的整个生命周期,不是某个具体 API 的用法。

为什么错误处理需要专门的原则

大部分代码把精力花在“正常路径怎么走通”,错误路径则随手 catch 一下了事。结果常见三种坏味道:

坏味道 表现 后果
吞异常 catch {} 空块 问题静默发生,现场全丢
过度捕获 顶层一个 catch 全接 无法区分“该重试”和“该放弃”
错误信息无用 throw new Error("失败") 现场要靠猜

三种坏味道有个共同的根源:写代码时只想着“别崩”,没想着“坏了怎么收拾”。 异常被当成人人都想绕开的麻烦,而不是设计的一部分。原则存在的意义,就是把失败路径拉回和正常路径同等的地位。

五个维度

结合技能体量与工程共识,一份错误处理原则技能一般覆盖五个维度:

维度 要点 反例信号
分类 哪些错误可重试、哪些不可、哪些该升级 所有错误走同一条重试逻辑
边界 在哪一层捕获、哪一层放行 每层都 catch,每层都吞
上下文 错误对象要带什么信息才有诊断价值 消息只有“失败”两个字
恢复 失败后状态如何回滚或保持一致 失败后留下半写坏的数据
呈现 给用户看什么、给日志写什么 用户看到堆栈,日志只有一句出错了

五个维度里最常被跳过的是恢复。多数设计只回答“怎么报错”,不回答“报完错系统处于什么状态”——半写坏的记录、没释放的锁、前后不一致的缓存,都是这一步欠的债。

dsh 版本会结合它自己的 abort / signal 机制与 Fiber 生命周期来谈,这也是它区别于通用原则的地方:取消传播算哪类错误、Fiber 销毁时未完成的异步怎么收尾,都是它有具体语境的场景。

一个对照例子

原则落到文字上,差别立见。

反面示例:

try {
  await save(order)
} catch (e) {
  console.log(e)
}

问题:异常被吞,用户不知道下单失败;日志只有一句出错了,线上排查要猜是网络、磁盘还是校验。

正面示例:

try {
  await save(order)
} catch (e) {
  throw new OrderError("下单保存失败", {
    cause: e,
    orderId: order.id,
    stage: "persist",
  })
}

区别在三点:① 不吞,向上抛;② 错误带 orderId 与 stage,现场可定位;③ cause 保留原始错误链。这正是五个维度里“上下文”与“边界”的体现:捕获只是为了补充信息,不是为了消灭错误。

写 catch 前三问

下次写 catch 之前,问自己三个问题:

1. 这个异常我打算重试、放行还是吞掉?
2. 如果吞掉,出问题时我靠什么定位?
3. 用户看到的消息,能告诉他下一步做什么吗?

三个都答不上来,就说明这个 catch 还没写完。

这三问正好对应上面的维度:第一问是分类,第二问是上下文,第三问是呈现。边界和恢复不在三问里,因为那俩是设计阶段的事,不该留到写 catch 时才想——真到写 catch 才发现没想清楚边界,通常意味着这一层就不该捕获。

一份错误处理自检清单

评审或自查时,对着下面几条过一遍:

检查项 过了吗
每一处 catch 都明确分类了(重试 / 放行 / 上报) □
没有空 catch 块 □
错误信息含定位字段(id / 阶段 / 输入特征) □
失败后系统状态可回滚或一致 □
用户消息说清“下一步做什么” □
取消 / 超时走了专门的错误类型,不是通用异常 □

最后一条和 dsh 的 Fiber 生命周期相关:一个被取消的请求不该被当成“成功一半”来处理,它需要一个明确的取消态,而不是混进普通异常里被重试。

你要不要装

你的情况 建议
代码里 catch 很多 建议,拿来审查一遍
在做错误码 / 重试设计 建议
脚本型小工具 不必

小结

  • dsh-error-handling 是短小精悍的错误处理原则技能,约 2389 字节。
  • 三种坏味道:吞异常、过度捕获、错误信息无用。
  • 五个维度:分类、边界、上下文、恢复、呈现。
  • 写 catch 前问三句:重试还是放行、吞了怎么定位、用户看到什么。

延伸阅读