orion-error 0.8.2

Structured error governance for layered Rust systems
Documentation
# 从方法论到工程实践: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 实现**。