# 从方法论到工程实践:Wukong 错误治理模型的 Rust 落地
很多系统后期越来越难维护,不是因为业务一定复杂,而是因为错误处理从来没有被认真设计过。
在原型阶段,错误处理通常只是"哪里失败就往上抛"。但系统一旦进入长期演进,失败路径就不再只是局部控制流。它要支撑重试、降级、告警、用户响应、排障和兼容性。到了这个阶段,错误如果仍然只是散乱字符串、局部 `enum` 或临时包装,系统就会越来越脆。
从系统构建者的角度看,核心问题并不是"怎么抛错误",而是:**如何让错误在治理层面收敛,在诊断层面保留有效信息。**
---
## 核心矛盾:收敛 vs. 诊断
调用方需要稳定、有限的分类,才能做治理决策,比如重试、降级、告警和边界映射。排障方需要完整、可靠的信息,才能定位根因。
这两类需求都合理,但天然拉向不同方向。
- 如果错误把所有底层技术细节都直接暴露给上层,调用方就会依赖数据库、网络库、第三方 SDK 的具体失败形态,边界很快被实现细节穿透。
- 如果错误只保留上层业务分类,排障时又会失去关键路径:原始失败是什么、发生在哪个组件、经过了哪些层、每层追加了什么上下文。
很多语言和生态都在尝试解决这个问题。Java 有异常和错误码,Go 有 `error`、wrapping 和 `errors.Is`,Rust 有 `Result<T, E>`、`?`、枚举和类型系统。这些机制都很重要,但它们解决的是"怎么表达错误",不自动解决"怎么治理错误"。
工具给的是砖块,不是建筑。
---
## Wukong 模型:把契约、诊断、输出拆开
我把这套方法叫作 **Wukong 错误治理模型**。
它的核心思想很简单:**用稳定契约让错误现形,用可靠诊断看清根因,用适配输出让不同接收端拿到稳定、合适的信息。**
它把错误拆成三个层次:
```text
错误内部模型 = 契约通道 + 诊断通道
适配输出 = 对内部模型按策略生成的不同输出视图
```
- 契约通道:稳定错误标识、稳定分类、治理属性。服务调用方、监控、网关和自动化决策。
- 诊断通道:原因链、上下文、细节。服务开发者、SRE 和排障工具。
- 适配输出:HTTP、RPC、CLI、日志、指标等边界上的不同输出视图。
关键不在于把错误做得更复杂,而在于把原本互相牵制的信息分层处理:该稳定的稳定下来,该动态的动态保留,该对外暴露的统一按策略生成。
---
## Rust 为什么适合做这件事
Rust 在语言层面给了很好的基础:
- `Result<T, E>` 让失败路径显式存在
- `enum` 适合表达有限分类空间
- `match` 提供穷尽检查
- 泛型适合抽象统一载体
但 Rust 不会自动完成治理。`thiserror`、`anyhow`、`eyre` 都很好用,但它们主要解决的是类型生成、快速传播和诊断体验,不会自动替团队定义稳定错误标识、语义边界和边界输出策略。
如果没有额外的工程约束,系统最终还是会退化成:
- 有些地方返回业务 `enum`
- 有些地方直接透传底层错误
- 有些地方把错误拼成字符串
- handler 各自决定 HTTP 状态码和错误消息
这时表面上用了 Rust 的错误生态,实际上仍然没有形成错误治理。
---
## 在 Rust 里落地,需要五条设计规则
## 1. 按语义域定义 Reason,而不是一个全局大枚举
不要让一个 `AppError` 包揽全系统所有失败。更合理的做法是按语义域建模:`RepositoryReason`、`OrderReason`、`ParserReason` 各自约束自己的分类空间。
```rust
#[derive(Debug, Clone, OrionError)]
enum OrderReason {
#[orion_error(identity = "order.submit_dependency_unavailable")]
SubmitDependencyUnavailable,
#[orion_error(identity = "order.invalid_state")]
InvalidState,
}
```
这里真正稳定的不是错误文案,也不是 enum 名字,而是错误标识。
## 2. 首次进入即结构化
I/O、网络、数据库、解析错误第一次进入体系时,就应同时完成三件事:
- 选择当前层分类
- 给出当前层解释
- 保留底层错误为 source
不要先把底层错误转成字符串,再到上层重新猜它是什么。那样治理和诊断都会一起退化。
## 3. 跨语义域传播时,建立新边界,但保留下层错误
同语义域内可以做分类收敛;跨语义域时,应建立新的业务语义边界。
例如 repository 层的连接失败,传播到 order service 时,不应该直接暴露成 repository 错误,而应该变成业务层可治理的 `order.submit_dependency_unavailable`。但 repository 层的原始错误仍要保留在 source chain 中,供排障追溯。
## 4. 边界只做输出,不重新解释错误
HTTP handler、RPC endpoint、CLI 入口不应该各自决定状态码、消息和日志级别。边界层应该只做一件事:把内部错误交给集中策略,生成该边界需要的输出视图。
这样同一个错误在所有边界上的对外行为才能一致。
## 5. 测试错误标识,不测试错误文案
真正稳定的契约是错误标识和策略结果,不是某句 detail 文案。
```rust
assert_eq!(
err.identity_snapshot().code,
"order.submit_dependency_unavailable"
);
```
如果测试断言的是错误消息,那每次文案优化、脱敏或翻译都会变成兼容性风险。
---
## `orion-error` 想解决的是什么
`orion-error` 不是一个单纯帮你少写样板代码的 crate。它要解决的是:在 Rust 工业级系统里,错误如何跨层传播、如何长期兼容、如何对外输出、如何支撑诊断。
可以把它理解为:
```text
Result<T, StructError<R>>
R -> 契约通道:reason / identity / category
StructError<R> -> 诊断通道:detail / context / source chain
policy -> 边界输出策略
```
这个结构的重点不在某个单独 API,而在于它把错误治理收敛成一套统一模型。
---
## 结语
Rust 已经给了我们很好的错误处理基础,但工业级系统真正需要的,不只是 `Result`、`?` 和几个方便的 derive 宏。
真正需要的是一套能长期约束团队实践的错误治理结构:让调用方拿到稳定契约,让排障方拿到有效信息,让边界输出保持一致,让系统在演进中不因为失败路径失控而不断腐化。
这也是我为什么把 `orion-error` 定位为 **Wukong 错误治理模型的 Rust 实现**。