名称: m13-领域错误处理 描述: “用于设计领域错误处理。关键词:领域错误、错误分类、恢复策略、重试、熔断器、优雅降级、领域错误层次结构、面向用户与内部错误、错误代码设计、错误上下文、退避重试、错误恢复、瞬时错误与永久错误” 用户可调用: false
领域错误处理策略
第2层:设计选择
核心问题
谁需要处理这个错误?他们应该如何恢复?
设计错误类型前:
- 这是面向用户的还是内部的?
- 恢复是否可能?
- 调试需要什么上下文?
错误分类
| 错误类型 | 受众 | 恢复方式 | 示例 |
|---|---|---|---|
| 面向用户 | 最终用户 | 引导操作 | 无效邮箱、未找到 |
| 内部错误 | 开发者 | 调试信息 | 数据库错误、解析错误 |
| 系统错误 | 运维/SRE | 监控/告警 | 连接超时、限流 |
| 瞬时错误 | 自动化 | 重试 | 网络错误、服务不可用 |
| 永久错误 | 人工 | 调查 | 配置无效、数据损坏 |
思考提示
设计错误类型前:
-
谁看到这个错误?
- 最终用户 → 友好消息,可操作
- 开发者 → 详细,可调试
- 运维 → 结构化,可告警
-
我们能恢复吗?
- 瞬时错误 → 退避重试
- 可降级 → 回退值
- 永久错误 → 快速失败,告警
-
需要什么上下文?
- 调用链 → anyhow::Context
- 请求ID → 结构化日志
- 输入数据 → 错误负载
向上追溯 ↑
到领域约束(第3层):
"我应该如何处理支付失败?"
↑ 提问:业务规则对重试有何要求?
↑ 检查:domain-fintech(交易要求)
↑ 检查:SLA(可用性要求)
| 问题 | 追溯至 | 提问 |
|---|---|---|
| 重试策略 | domain-* | 重试可接受的延迟是多少? |
| 用户体验 | domain-* | 用户应该看到什么消息? |
| 合规性 | domain-* | 审计必须记录什么? |
向下追溯 ↓
到实现(第1层):
"需要类型化错误"
↓ m06-error-handling:库使用thiserror
↓ m04-zero-cost:错误枚举设计
"需要错误上下文"
↓ m06-error-handling:anyhow::Context
↓ 日志记录:带字段的tracing
"需要重试逻辑"
↓ m07-concurrency:异步重试模式
↓ 库:tokio-retry, backoff
快速参考
| 恢复模式 | 适用场景 | 实现方式 |
|---|---|---|
| 重试 | 瞬时故障 | 指数退避 |
| 回退 | 降级模式 | 缓存/默认值 |
| 熔断器 | 级联故障 | failsafe-rs |
| 超时 | 慢操作 | tokio::time::timeout |
| 舱壁隔离 | 隔离 | 独立线程池 |
错误层次结构
#[derive(thiserror::Error, Debug)]
pub enum AppError {
// 面向用户
#[error("无效输入: {0}")]
Validation(String),
// 瞬时错误(可重试)
#[error("服务暂时不可用")]
ServiceUnavailable(#[source] reqwest::Error),
// 内部错误(记录详情,显示通用)
#[error("内部错误")]
Internal(#[source] anyhow::Error),
}
impl AppError {
pub fn is_retryable(&self) -> bool {
matches!(self, Self::ServiceUnavailable(_))
}
}
重试模式
use tokio_retry::{Retry, strategy::ExponentialBackoff};
async fn with_retry<F, T, E>(f: F) -> Result<T, E>
where
F: Fn() -> impl Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
let strategy = ExponentialBackoff::from_millis(100)
.max_delay(Duration::from_secs(10))
.take(5);
Retry::spawn(strategy, || f()).await
}
常见错误
| 错误做法 | 为何错误 | 更好做法 |
|---|---|---|
| 所有错误相同 | 不可操作 | 按受众分类 |
| 重试所有错误 | 浪费资源 | 仅重试瞬时错误 |
| 无限重试 | 自我DoS | 最大尝试次数 + 退避 |
| 暴露内部错误 | 安全风险 | 用户友好消息 |
| 无上下文 | 难以调试 | 处处使用.context() |
反模式
| 反模式 | 为何不好 | 更好做法 |
|---|---|---|
| 字符串错误 | 无结构 | thiserror类型 |
| 对可恢复错误panic! | 用户体验差 | 带上下文的Result |
| 忽略错误 | 静默失败 | 记录或传播 |
| 处处使用Box<dyn Error> | 丢失类型信息 | thiserror |
| 在正常路径中处理错误 | 性能差 | 早期验证 |
相关技能
| 何时需要 | 参见 |
|---|---|
| 错误处理基础 | m06-error-handling |
| 重试实现 | m07-concurrency |
| 领域建模 | m09-domain |
| 面向用户的API | domain-* |