# 从方法论到工程实践:Wukong 错误治理模型的 Rust 落地
很多系统后期不好维护,不是因为业务一定复杂,而是因为错误处理从来没有被当成系统设计的一部分。
原型阶段,错误处理往往只是"出了问题就往上抛"。但一旦系统进入长期演进,失败路径就不再只是局部控制流。它要支撑重试、降级、告警、用户响应、排障和兼容性。这时,错误如果仍然只是散乱字符串、局部 `enum` 或临时包装,系统就会越来越脆。
这篇文章只讨论一个问题:**从 Rust 工程实践看,系统应该怎样设计错误治理?**
---
## 核心问题不是"怎么抛",而是怎么治理
错误处理的真正矛盾,不在于你选异常、返回值还是 `Result`,而在于这两类需求怎么同时满足:
- 调用方需要稳定、有限的分类,才能做重试、降级、告警和边界映射。
- 排障方需要完整、可靠的信息,才能定位根因。
这就是错误治理的核心矛盾:
**如何让错误在治理层面收敛,在诊断层面保留有效信息。**
如果只保留技术异常,信息很全,但上层没法稳定治理;如果只保留业务分类,调用方舒服了,排障时又会丢根因。很多系统就是在这两个方向之间反复摆动,最后既不稳定,也不透明。
---
## Wukong 模型:契约、诊断、输出分层
我把这套方法叫作 **Wukong 错误治理模型**。
它的核心思想很简单:**用稳定契约让错误现形,用可靠诊断看清根因,用适配输出让不同接收端拿到稳定、合适的信息。**
在这个模型里,错误内部不是一个混成对象,而是三个层次:
```text
错误内部模型 = 契约通道 + 诊断通道
适配输出 = 对内部模型按策略生成的不同输出视图
```
- 契约通道:稳定错误标识、稳定分类、治理属性。服务调用方、网关、监控和自动化决策。
- 诊断通道:原因链、上下文、细节。服务开发者、SRE 和排障工具。
- 适配输出:HTTP、RPC、CLI、日志、指标等边界上的不同输出视图。
关键点不在于把错误做得更复杂,而在于把本来互相牵制的信息拆开:该稳定的稳定下来,该动态的动态保留,该对外暴露的统一按策略生成。
---
## Rust 很适合这件事,但不会自动完成它
Rust 的类型系统天然适合错误治理。
- `Result<T, E>` 让失败路径显式存在。
- `enum` 适合表达有限分类空间。
- `match` 提供穷尽检查。
- 泛型适合抽象统一载体。
但 Rust 只解决了"怎么表达失败",没有自动解决"怎么治理失败"。
`thiserror`、`anyhow`、`eyre` 都很好用,但它们主要解决的是类型生成、快速传播和诊断体验,不会自动替你定义稳定错误标识、语义边界和边界输出策略。工具给的是砖块,不是建筑。
---
## 在 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 入口不应该各自决定状态码、消息和日志级别。边界层应该只做一件事:把内部错误交给集中策略,生成该边界需要的输出视图。
这能避免同一个错误在不同 handler 里被解释成不同的对外行为。
## 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 -> 边界输出策略
```
在这个结构里:
- `R` 负责表达稳定分类和错误标识
- `StructError<R>` 负责承载诊断信息
- exposure / report / snapshot 负责把内部模型投影成不同边界视图
它的目标不是替代 Rust 现有错误生态,而是在工业级系统里,把"错误如何跨层传播、如何长期兼容、如何对外输出、如何支撑诊断"这几件事收敛到一套统一模型中。
---
## 结语
Rust 在语言层面已经给了我们很好的错误处理基础,但工业级系统真正需要的,不只是 `Result`、`?` 和几个方便的 derive 宏。
我们真正需要的是一套能长期约束团队实践的错误治理结构:让调用方拿到稳定契约,让排障方拿到有效信息,让边界输出保持一致,让系统在演进中不因为失败路径失控而不断腐化。
这也是我为什么把 `orion-error` 定位为 **Wukong 错误治理模型的 Rust 实现**。