必须显式传递原始状态码作为cause值而非拼入message,逐层封装保持cause链完整,监控系统需解析cause链提取code打点,旧环境需降级兼容并跨服务透传root_code。

不能靠自动继承,必须在每一层错误构造时显式传入原始状态码作为 cause 的一部分,同时配合结构化错误对象封装,才能让监控系统真正追溯到业务源头。
把状态码嵌入 cause,而不是丢进 message
原始状态码(如 "BUSINESS_001"、409 或 { code: "PAY_TIMEOUT", detail: "third-party timeout" })要作为 cause 的值传入,而不是拼进错误消息里。字符串拼接会丢失类型和可解析性,监控系统无法提取结构化字段。
- ✅ 正确:抛出新错误时,用
{ cause: { code: "ORDER_INVALID", httpStatus: 400 } } - ❌ 错误:写成
new Error("订单校验失败,状态码:ORDER_INVALID")—— 日志里只剩文本,无法被自动归类或告警 - ⚠️ 注意:
cause可以是任意值,不强制是Error实例;业务状态码本身就可以是轻量对象,便于序列化与传输
多层包装时保持 cause 链完整
从 DAO 层到 API 层每经过一次封装,都要把上层的 cause(含原始状态码)继续向上传递,形成“状态码→业务异常→网关异常→前端错误”的可展开链路。
- DAO 层捕获数据库错误 → 包装为
new Error("库存扣减失败", { cause: { code: "DB_LOCK_TIMEOUT", retryable: true } }) - Service 层捕获该错误 → 再包装为
new Error("下单失败", { cause: prevErr }),此时prevErr.cause仍保留原始状态码 - Controller 层统一拦截 → 提取最深层
getRootCause(),从中读取code字段用于打点上报
监控系统需主动解析 cause 链,而非只看顶层 error
默认日志采集(如 winston、pino)或 APM 工具(如 Sentry、Datadog)通常只序列化顶层 error.stack 和 error.message,cause 字段会被忽略。必须显式增强:
- 自定义日志格式:遍历
err.cause直到尽头,提取所有code、httpStatus、retryable等字段,平铺为日志上下文 - 埋点上报时,用
getRootCause(err)?.code作为核心指标标签,而非err.name - 前端上报错误时,通过
JSON.stringify(err, (k, v) => k === 'cause' && v && typeof v === 'object' ? { ...v, _isCause: true } : v)保留 cause 结构
兼容性兜底与跨环境传递
Error.prototype.cause 在旧运行时(如 Node.js <16.9、Safari ≤16.6、微信小程序基础库 ≤2.28.1)会被静默忽略,导致链路断裂。需双轨保障:
- 运行时检测:
if ('cause' in Error.prototype)再启用 cause 构造;否则退化为在error.detail = { originalCode: ..., stack: ... }中手动挂载 - 跨服务调用时,cause 天然失效,需将最深层状态码提取后注入 HTTP Header(如
X-Root-Error-Code: PAY_TIMEOUT)或响应体error.root_code字段 - 全链路追踪中,把
root_code作为 trace tag 上报,与 TraceID 关联,实现指标 + 链路 + 日志三者归因统一

















