🦀 Rust 错误处理深度指南

错误处理是 Rust 最独特的设计之一:没有异常,没有 null,用类型系统强制你面对失败。这不是负担——这是编译器在替你排雷。从 panic! 到 anyhow,这篇文章覆盖你需要的全部。

panic vs Result ? 运算符 thiserror anyhow 错误分层 异步错误

📑 目录

  1. Rust 错误处理哲学
  2. panic! 与不可恢复错误
  3. Result<T, E> 详解
  4. ? 运算符与错误传播
  5. Option<T> 与空值处理
  6. 自定义错误类型
  7. anyhow:应用层错误处理
  8. 错误分层架构
  9. 异步错误处理
  10. 实战:构建多层错误处理框架

1. Rust 错误处理哲学

Rust 将错误分为两大类,这是它与其他语言最根本的区别:

💥
不可恢复错误
panic!
🔄
可恢复错误
Result<T,E>

与其他语言的对比

✅ Rust 的方式

// 错误是类型的一部分
fn read_file(path: &str)
    -> Result<String, io::Error>
{
    let content = fs::read_to_string(path)?;
    Ok(content)
}

// 调用者必须处理错误
match read_file("config.toml") {
    Ok(s) => println!("{s}"),
    Err(e) => eprintln!("Error: {e}"),
}

❌ 异常的方式 (Java/Python/C++)

# 错误是隐式的,调用者可能忽略
def read_file(path):
    with open(path) as f:
        return f.read()

# 没有强制处理,可能突然崩溃
content = read_file("config.toml")
# 如果文件不存在 → RuntimeError
# 但函数签名完全看不出来

核心原则

🎯 显式优于隐式

错误是函数签名的一部分。Result 告诉你"这里可能失败",你无法假装没看见。编译器是你的盟友。

🧩 类型驱动决策

panic vs Result 不是随意选择——它是设计决策。如果你选 Result,调用者必须处理;如果你选 panic,说明这种情况"不该发生"。

⚡ 零成本抽象

Result 是普通枚举,? 运算符编译为普通的分支跳转。没有异常表、没有栈展开(正常路径),性能完全可预测。

Rust 的洞察:大多数语言的错误处理问题不是"该用什么机制",而是"调用者不知道需要处理错误"。Rust 把这个信息编码进类型系统,让不可能被忽略。

决策树:panic 还是 Result?

┌─────────────────────┐ │ 这个错误可以被 │ │ 调用者合理处理吗? │ └────────┬────────────┘ │ ┌────────────┴────────────┐ │ │ 是 否 │ │ ▼ ▼ ┌───────────────┐ ┌────────────────────┐ │ Result<T,E> │ │ 这是编程错误还是 │ │ 让调用者决定 │ │ 外部不可控故障? │ └───────────────┘ └────────┬───────────┘ │ ┌────────────┴────────────┐ │ │ 编程错误 外部故障 │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ panic! │ │ Result 或 │ │ (bug = 崩溃) │ │ abort 进程 │ └──────────────┘ └──────────────┘

2. panic! 与不可恢复错误

panic! 表示程序遇到了不可能恢复的状态——这通常意味着代码有 bug,继续运行只会产生更严重的问题。

何时应该 panic

场景示例原因
数组越界v[100] 在长度为 5 的 Vec 上逻辑 bug,无法继续
除以零1 / 0数学上不可能
不变量被破坏解析后仍 None逻辑假设不成立
初始化失败数据库连接池创建失败程序无法启动
示例/原型代码unwrap() 在 demo 中简化代码,非生产

panic 的两种策略:unwind vs abort

unwind(默认)

// Cargo.toml
[profile.dev]
panic = "unwind"  # 默认

// 行为:
// 1. 打印 panic 信息
// 2. 栈展开(调用 Drop)
// 3. 释放资源
// 4. 退出线程

// 优点:资源可清理、可 catch_unwind
// 缺点:二进制更大、稍慢

abort

// Cargo.toml
[profile.release]
panic = "abort"

// 行为:
// 1. 打印 panic 信息
// 2. 直接终止进程
// 3. 不调用 Drop

// 优点:更小二进制、更快
// 缺点:无清理、无法 catch

⚠️ 嵌入式 / 高性能场景:生产发布时 panic = "abort" 可以显著减小二进制体积(去掉 unwind 表),但代价是 panic 时不会调用析构函数。如果你依赖 RAII 清理,需谨慎。

catch_unwind:有限的安全网

catch_unwind 用法
use std::panic;

let result = panic::catch_unwind(|| {
    println!("before panic");
    panic!("oh no!");
    println!("never reached");
});

match result {
    Ok(_) => println!("正常完成"),
    Err(_) => println!("捕获到 panic!"),
}

// 输出:
// before panic
// 捕获到 panic!

🚨 catch_unwind 不是 try-catch! 它的设计用途仅限于:

  • FFI 边界:防止 Rust panic 跨越 C ABI
  • 测试框架:捕获测试中的 panic
  • 服务器:防止一个请求的 panic 炸掉整个进程

不要用它做常规错误控制流——那是 Result 的工作。

3. Result<T, E> 详解

Result 是 Rust 错误处理的基石——一个简单的枚举,却承载了丰富的组合子语义。

enum Result<T, E> { Ok(T), // 成功,携带值 T Err(E), // 失败,携带错误 E } // 编译器强制你处理两种情况 // 忘了?编译错误!

核心组合子

组合子签名用途
mapResult<T,E> → (T→U) → Result<U,E>转换成功值
map_errResult<T,E> → (E→F) → Result<T,F>转换错误
and_thenResult<T,E> → (T→Result<U,E>) → Result<U,E>链式操作(flatMap)
or_elseResult<T,E> → (E→Result<T,F>) → Result<T,F>错误恢复
unwrapResult<T,E> → T断言成功,否则 panic
expectResult<T,E> → &str → T同上,带自定义消息
unwrap_orResult<T,E> → T → T失败时提供默认值
unwrap_or_elseResult<T,E> → (E→T) → T失败时计算默认值
unwrap_or_defaultResult<T,E> → T失败时用 Default
is_ok / is_errResult<T,E> → bool判断状态
ok / errResult<T,E> → Option<T> / Option<E>转为 Option

组合子实战:函数式错误处理

用组合子代替嵌套 match
use std::fs;

// ❌ 嵌套 match —— 金字塔厄运
fn parse_config_v1(path: &str) -> Result<Config, AppError> {
    match fs::read_to_string(path) {
        Ok(content) => match toml::from_str(&content) {
            Ok(config) => Ok(config),
            Err(e) => Err(AppError::Parse(e)),
        },
        Err(e) => Err(AppError::Io(e)),
    }
}

// ✅ 组合子风格 —— 线性、可读
fn parse_config_v2(path: &str) -> Result<Config, AppError> {
    fs::read_to_string(path)
        .map_err(AppError::Io)
        .and_then(|content| {
            toml::from_str(&content)
                .map_err(AppError::Parse)
        })
}

unwrap vs expect:何时使用?

⚠️ 生产代码原则:

  • unwrap() —— 仅在"不可能失败"时使用(如 Ok(42)
  • expect("reason") —— 优于 unwrap,因为消息解释了"为什么不可能失败"
  • 测试代码中随意用 unwrap(),没毛病
  • 生产代码中 expect() 优于 unwrap(),它提供调试上下文
expect 的正确用法
// ✅ 好:解释了为什么这里不可能失败
let port: u16 = env::var("PORT")
    .expect("PORT must be set by the deployment script");

// ❌ 差:没有上下文
let port: u16 = env::var("PORT").unwrap();

// ✅ 测试中:随意 unwrap
#[test]
fn test_addition() {
    assert_eq!(parse("1+2").unwrap(), 3);
}

4. ? 运算符与错误传播

? 是 Rust 最优雅的语法糖——它把"检查错误、提前返回"压缩成一个字符。

展开来看

? 的等价展开
// 这一行:
let content = fs::read_to_string(path)?;

// 等价于:
let content = match fs::read_to_string(path) {
    Ok(v) => v,
    Err(e) => return Err(e.into()),
};

注意 .into()——? 会自动做类型转换,前提是错误类型实现了 Into。这让它可以跨错误类型传播。

链式调用

? 链——极致简洁
use std::fs;
use std::io;

// 无 ? 版本
fn read_and_parse(path: &str) -> Result<Config, io::Error> {
    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => return Err(e),
    };
    let config = match parse(&content) {
        Ok(c) => c,
        Err(e) => return Err(e),
    };
    Ok(config)
}

// ? 链版本
fn read_and_parse(path: &str) -> Result<Config, io::Error> {
    let content = fs::read_to_string(path)?;
    let config = parse(&content)?;
    Ok(config)
}

// 甚至一行搞定
fn read_and_parse(path: &str) -> Result<Config, io::Error> {
    parse(&fs::read_to_string(path)?)
}

在 Option 中使用 ?

💡 ? 也适用于 Option! 在返回 Option 的函数中,? 会在 None 时提前返回 None

Option 上的 ?
fn last_char_of_first_line(text: &str) -> Option<char> {
    let first_line = text.lines().next()?;  // None → return None
    first_line.chars().last()               // Some(c) or None
}

try 块(实验性)

try 块 —— nightly 特性
#![feature(try_blocks)]

fn try_operation() -> Result<i32, io::Error> {
    let result: Result<i32, io::Error> = try {
        let a = fs::read_to_string("a.txt")?;
        let b = fs::read_to_string("b.txt")?;
        a.len() + b.len()
    };
    result
}

// try 块让你在同一个函数中使用 ?
// 而不需要每个子函数都返回 Result

⚠️ try 块目前仍为 nightly 特性#![feature(try_blocks)])。稳定前,使用闭包作为替代:

闭包模拟 try 块
// 稳定版替代方案:闭包 + ?
let result = (|| -> Result<_, io::Error> {
    let a = fs::read_to_string("a.txt")?;
    let b = fs::read_to_string("b.txt")?;
    Ok(a.len() + b.len())
})();

5. Option<T> 与空值处理

Rust 没有 nullOption<T> 就是 Rust 对"值可能不存在"的回答。

enum Option<T> { Some(T), // 有值 None, // 无值 } // Tony Hoare(null 发明者)称之为"十亿美元错误" // Rust:用类型系统消灭它

核心组合子

组合子签名用途
mapOption<T> → (T→U) → Option<U>转换 Some 中的值
and_thenOption<T> → (T→Option<U>) → Option<U>链式操作
or_elseOption<T> → (→Option<T>) → Option<T>None 时的备选方案
unwrap_orOption<T> → T → T提供默认值
unwrap_or_elseOption<T> → (→T) → T延迟计算默认值
unwrap_or_defaultOption<T> → TDefault trait 默认值
ok_orOption<T> → E → Result<T,E>转 Result:None→Err
ok_or_elseOption<T> → (→E) → Result<T,E>延迟计算错误
filterOption<T> → (&T→bool) → Option<T>条件过滤
is_some / is_noneOption<T> → bool判断

Option vs Result 互转

Option ↔ Result 转换
// Option → Result
fn find_user(id: u64) -> Option<User> { ... }

// 需要给 None 一个有意义的错误
let user = find_user(42)
    .ok_or(AppError::UserNotFound(42))?;

// Result → Option(丢弃错误信息)
let maybe_port: Option<u16> = env::var("PORT")
    .ok()                    // Err → None
    .and_then(|s| s.parse().ok());  // ParseError → None

// ⚠️ ok() 丢弃了错误,只在"不关心错误原因"时使用

实战模式

Option 链式处理
struct Config {
    database_url: Option<String>,
    pool_size: Option<usize>,
    timeout_secs: Option<u64>,
}

// 环境变量覆盖配置
fn resolve_pool_size(config: &Config) -> usize {
    env::var("DB_POOL_SIZE")
        .ok()
        .and_then(|s| s.parse().ok())
        .or(config.pool_size)
        .unwrap_or(10)  // 最终默认值
}

// filter:条件过滤
fn find_admin(users: &[User]) -> Option<&User> {
    users.iter()
        .find(|u| u.is_admin)
        .filter(|u| u.is_active)  // 找到 admin 且必须是 active
}

✅ 设计原则:当"值不存在"是正常的、预期的情况时,用 Option。当"值不存在"意味着错误或意外,用 Result

6. 自定义错误类型

当你的项目变复杂,io::Error 已经不够用了。你需要自己的错误类型——它能携带上下文、支持错误链、给用户清晰的反馈。

手动实现 Error trait

手动定义错误类型
use std::fmt;
use std::error::Error;
use std::io;

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Config(String),
    Database(DbError),
    NotFound { resource: String, id: u64 },
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {e}"),
            AppError::Config(msg) => write!(f, "Config error: {msg}"),
            AppError::Database(e) => write!(f, "Database error: {e}"),
            AppError::NotFound { resource, id } => {
                write!(f, "{resource} with id {id} not found")
            }
        }
    }
}

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            AppError::Database(e) => Some(e),
            _ => None,
        }
    }
}

// 为每个子错误实现 From
impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self { AppError::Io(e) }
}

impl From<DbError> for AppError {
    fn from(e: DbError) -> Self { AppError::Database(e) }
}

thiserror:告别样板代码

thiserror——同样的事情,1/5 的代码
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] io::Error),          // auto From + source

    #[error("Config error: {0}")]
    Config(String),

    #[error("Database error: {0}")]
    Database(#[from] DbError),      // auto From + source

    #[error("{resource} with id {id} not found")]
    NotFound { resource: String, id: u64 },
}

thiserror 为你自动生成了 DisplayErrorFrom 实现。#[from] 同时做了两件事:生成 From impl 和设置 source()

error-set:更简洁的错误集合

error-set —— 错误集合的新选择
use error_set::error_set;

error_set! {
    AppError = {
        Io(std::io::Error),
        Config(String),
        Database(DbError),
        NotFound { resource: String, id: u64 },
    }
}

// error_set 自动生成:
// - Display / Error impls
// - From 转换
// - 错误子集(可以定义分层错误)
// - 比手写 thiserror 更少的样板

source chain:错误链追踪

遍历错误链
use std::error::Error;

fn print_error_chain(err: &dyn Error) {
    let mut source = Some(err);
    let mut depth = 0;
    while let Some(e) = source {
        println!("{}{}", "  ".repeat(depth), e);
        source = e.source();
        depth += 1;
    }
}

// 输出示例:
// Database error: connection refused
//   Connection error: tcp connect failed
//     IO error: Connection refused (os error 111)

✅ 库 crate 用 thiserror,应用 crate 用 anyhow——这是 Rust 社区的共识。库需要精确的错误类型供调用者匹配;应用只需要把错误一路传播到顶层。

7. anyhow:应用层错误处理

anyhow 是应用代码的瑞士军刀——不需要定义错误类型,? 万物皆可传播,需要时还能加上下文。

核心用法

anyhow 基础
use anyhow::{Context, Result, anyhow, bail};

fn read_config(path: &str) -> Result<Config> {
    let content = fs::read_to_string(path)
        .context(format!("Failed to read config from {path}"))?;

    let config: Config = toml::from_str(&content)
        .context("Failed to parse config")?;

    if config.pool_size == 0 {
        bail!("pool_size cannot be zero");
    }

    Ok(config)
}

// 顶层处理
fn main() -> anyhow::Result<()> {
    let config = read_config("config.toml")?;
    run_server(config)?;
    Ok(())
}
// main 返回 anyhow::Result 时,错误会自动打印到 stderr

context:错误灵魂

💡 context 的价值:原始错误只告诉你"什么技术故障发生了"。context 告诉你"在做什么时发生了"。两者结合才有完整的调试信息。

context 链
use anyhow::{Context, Result};

fn process_order(order_id: u64) -> Result<Order> {
    let order = db::find_order(order_id)
        .context("Failed to fetch order from database")?;

    let user = db::find_user(order.user_id)
        .context("Failed to fetch order's user")?;

    let payment = payment::charge(&order, &user)
        .context(format!(
            "Payment failed for order {order_id}, user {}",
            user.name
        ))?;

    Ok(order)
}

// 错误输出示例:
// Payment failed for order 42, user Alice
//   Caused by:
//     0: Charge request failed
//     1: Connection refused (os error 111)

with_context:延迟计算

with_context vs context
// context() 立即构造字符串——即使成功也要花时间
.context(format!("Failed to process order {order_id}"))?

// with_context() 仅在失败时才执行闭包
.with_context(|| format!("Failed to process order {order_id}"))?

// 推荐在字符串构造有开销时使用 with_context

backtrace:错误栈追踪

启用 backtrace
// 1. Cargo.toml
[dependencies]
anyhow = { version = "1", features = ["backtrace"] }

// 2. 设置环境变量
// RUST_BACKTRACE=1

// 3. 错误中自动包含 backtrace
fn deep_function() -> Result<()> {
    Ok(fs::read_to_string("nonexistent")?)
}

// 错误输出包含完整调用栈:
// No such file or directory (os error 2)
// Stack backtrace:
//    0: anyhow::backtrace::capture::...
//    1: deep_function::...
//    2: main::...

anyhow vs thiserror 选择指南

anyhow(应用层)

// 什么时候用 anyhow?
// ✅ main 函数
// ✅ CLI 工具
// ✅ 应用逻辑(不需要
//    调用者匹配具体错误)
// ✅ 原型开发

fn main() -> anyhow::Result<()> {
    let config = read_config()?;
    run(config)?;
    Ok(())
}

thiserror(库层)

// 什么时候用 thiserror?
// ✅ 库 crate 的公开 API
// ✅ 调用者需要 match 错误
// ✅ 需要精确的错误类型
// ✅ 不同错误需要不同处理

#[derive(Error, Debug)]
pub enum DbError {
    #[error("Connection failed: {0}")]
    Connection(String),
    #[error("Query failed: {0}")]
    Query(String),
}

8. 错误分层架构

大型项目中,错误不是扁平的——它们有层次。每一层都应该有自己的错误类型,并在边界做转换。

四层模型

┌─────────────────────────────────────────────────┐ │ 用户层 (User) │ │ 人类可读消息 · 本地化 · 日志脱敏 │ └────────────────────┬────────────────────────────┘ │ 错误 → HTTP/CLI 响应 ┌────────────────────┴────────────────────────────┐ │ API 层 (API) │ │ HTTP 状态码 · 错误码 · 请求 ID │ └────────────────────┬────────────────────────────┘ │ ServiceError → ApiError ┌────────────────────┴────────────────────────────┐ │ 服务层 (Service) │ │ 业务逻辑错误 · 领域概念 · 不暴露实现细节 │ └────────────────────┬────────────────────────────┘ │ DbError → ServiceError ┌────────────────────┴────────────────────────────┐ │ 基础设施层 (Infra) │ │ DB · 网络 · 文件系统 · 第三方 API │ └─────────────────────────────────────────────────┘

crate 内部:细粒度错误

数据库 crate 的内部错误
// db-crate/src/error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DbError {
    #[error("Connection to {host}:{port} failed: {source}")]
    ConnectionFailed {
        host: String,
        port: u16,
        source: sqlx::Error,
    },

    #[error("Query timeout after {ms}ms: {query}")]
    QueryTimeout { ms: u64, query: String },

    #[error("Record not found: {table}/{id}")]
    NotFound { table: String, id: String },

    #[error("Unique constraint violated: {constraint}")]
    UniqueViolation { constraint: String },

    #[error(transparent)]
    Internal(#[from] sqlx::Error),
}

crate 边界:错误转换

服务层转换数据库错误
// service-crate/src/error.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ServiceError {
    #[error("User {user_id} not found")]
    UserNotFound { user_id: u64 },

    #[error("Email already registered")]
    DuplicateEmail,

    #[error("Service temporarily unavailable")]
    Unavailable,

    #[error("Internal error")]
    Internal,  // 不暴露 DB 细节!
}

// 显式转换:DB 错误 → 服务错误
impl From<DbError> for ServiceError {
    fn from(e: DbError) -> Self {
        match e {
            DbError::NotFound { table, id } if table == "users" => {
                ServiceError::UserNotFound {
                    user_id: id.parse().unwrap_or(0),
                }
            }
            DbError::UniqueViolation { ref constraint }
                if constraint.contains("email") => {
                ServiceError::DuplicateEmail
            }
            DbError::ConnectionFailed { .. }
            | DbError::QueryTimeout { .. } => {
                ServiceError::Unavailable
            }
            _ => ServiceError::Internal,
        }
    }
}

API 层:面向客户端的错误

API 层错误响应
// api-crate/src/error.rs
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Serialize;

#[derive(Serialize)]
struct ErrorResponse {
    error: ErrorBody,
}

#[derive(Serialize)]
struct ErrorBody {
    code: String,
    message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    request_id: Option<String>,
}

impl IntoResponse for ServiceError {
    fn into_response(self) -> Response {
        let (status, code, message) = match &self {
            ServiceError::UserNotFound { .. } => (
                StatusCode::NOT_FOUND,
                "USER_NOT_FOUND",
                "The requested user does not exist",
            ),
            ServiceError::DuplicateEmail => (
                StatusCode::CONFLICT,
                "DUPLICATE_EMAIL",
                "This email is already registered",
            ),
            ServiceError::Unavailable => (
                StatusCode::SERVICE_UNAVAILABLE,
                "SERVICE_UNAVAILABLE",
                "Service is temporarily unavailable",
            ),
            ServiceError::Internal => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "INTERNAL_ERROR",
                "An internal error occurred",
            ),
        };

        // 日志中记录完整错误,响应中只返回安全信息
        tracing::error!(error = %self, "Request failed");

        (status, axum::Json(ErrorResponse {
            error: ErrorBody {
                code: code.to_string(),
                message: message.to_string(),
                request_id: None,
            },
        })).into_response()
    }
}

🚨 安全原则:绝不把内部错误暴露给客户端!

  • 数据库连接字符串、SQL 语句、堆栈跟踪——这些只进日志,不进响应
  • 客户端只需要"发生了什么类型的问题"和"我能做什么"
  • 使用 #[error(transparent)] 在日志中保留完整链,但 API 响应必须脱敏

9. 异步错误处理

异步 Rust 的错误处理有一些独特的坑——JoinErrortimeout、cancellation 安全性。理解它们才能写出健壮的异步代码。

tokio::spawn 与 JoinError

JoinError 的两种来源
use tokio::task::JoinError;

// 1. 任务 panic
let handle = tokio::spawn(async {
    panic!("task crashed!");
});
let result = handle.await;  // Result<T, JoinError>
match result {
    Ok(value) => println!("成功: {value:?}"),
    Err(e) if e.is_panic() => {
        // 任务内部 panic 了
        // 注意:panic 信息被包装了
        println!("任务 panic: {e}");
    }
    Err(e) if e.is_cancelled() => {
        println!("任务被取消");
    }
    Err(e) => println!("Join 错误: {e}"),
}

// 2. 任务被取消(JoinHandle::abort)
handle.abort();

⚠️ tokio::spawn 要求 Send + 'static——意味着错误类型也必须满足。如果你的错误包含 !Send 类型(如 Rc),需要先转换为 Send 友好的形式。

timeout:时间约束

tokio::time::timeout
use tokio::time::{timeout, Duration};
use anyhow::{Context, Result};

async fn fetch_with_timeout(url: &str) -> Result<String> {
    timeout(Duration::from_secs(5), reqwest::get(url))
        .await
        .context("Request timed out after 5 seconds")?   // ← ElapsedError
        .context(format!("Failed to fetch {url}"))?
        .text()
        .await
        .context("Failed to read response body")
}

// timeout 返回 Result<T, ElapsedError>
// ElapsedError 需要二次处理内层 Result

cancellation 安全性

🚨 这是异步错误处理中最隐蔽的 bug 来源!当一个 Future 被 drop(如 timeout 触发、select! 切换),操作可能被半途放弃。

不安全 vs 安全的取消
use tokio::select;
use tokio::sync::mpsc;

// ❌ 不安全:可能丢失消息
async fn process_unsafe(mut rx: mpsc::Receiver<Msg>) {
    // 如果 select! 走了 timeout 分支,
    // rx.recv() 返回的 Some(msg) 就丢了!
    select! {
        msg = rx.recv() => {
            // 处理 msg
        }
        _ = tokio::time::sleep(Duration::from_secs(30)) => {
            // 超时
        }
    }
}

// ✅ 安全:使用 cancellation-safe 模式
async fn process_safe(mut rx: mpsc::Receiver<Msg>) {
    select! {
        msg = rx.recv() => {
            // recv() 是 cancellation-safe 的:
            // 如果被 drop,消息留在 channel 中不丢失
        }
        _ = tokio::time::sleep(Duration::from_secs(30)) => {
            // 超时
        }
    }
}

// ⚠️ 哪些操作是 cancellation-safe 的?
// ✅ recv()   — 消息留在 channel
// ✅ read()   — 数据留在 buffer
// ❌ send()   — 消息可能已发送但未确认
// ❌ write()  — 数据可能部分写入

select! 与错误聚合

select! 中的错误处理
use tokio::select;

async fn multi_source() -> Result<Data> {
    let mut db_fut = db::fetch();
    let mut cache_fut = cache::fetch();
    let mut api_fut = api::fetch();

    // 任一成功就返回,全部失败才报错
    let mut errors = Vec::new();

    loop {
        select! {
            result = &mut db_fut => {
                match result {
                    Ok(data) => return Ok(data),
                    Err(e) => {
                        tracing::warn!("DB failed: {e}");
                        errors.push(e.into());
                    }
                }
            }
            result = &mut cache_fut => {
                match result {
                    Ok(data) => return Ok(data),
                    Err(e) => {
                        tracing::warn!("Cache failed: {e}");
                        errors.push(e.into());
                    }
                }
            }
            result = &mut api_fut => {
                match result {
                    Ok(data) => return Ok(data),
                    Err(e) => {
                        tracing::warn!("API failed: {e}");
                        errors.push(e.into());
                    }
                }
            }
        }

        if errors.len() == 3 {
            anyhow::bail!("All sources failed: {:?}", errors);
        }
    }
}

10. 实战:构建多层错误处理框架

理论够了,让我们从零构建一个完整的三层错误处理框架:数据库层 → 服务层 → API 层。

项目结构

my-app/ ├── crates/ │ ├── db/ # 数据库层 │ │ └── src/ │ │ ├── lib.rs │ │ ├── error.rs ← DbError │ │ └── repo.rs │ ├── service/ # 服务层 │ │ └── src/ │ │ ├── lib.rs │ │ ├── error.rs ← ServiceError │ │ └── user.rs │ └── api/ # API 层 │ └── src/ │ ├── main.rs │ ├── error.rs ← ApiError │ └── handlers.rs └── Cargo.toml

Step 1: 数据库层错误

crates/db/src/error.rs
use sqlx::error::DatabaseError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DbError {
    #[error("Connection pool exhausted")]
    PoolExhausted,

    #[error("Record not found in {table}: {key}={value}")]
    NotFound {
        table: String,
        key: String,
        value: String,
    },

    #[error("Duplicate entry in {table}: {constraint}")]
    Duplicate {
        table: String,
        constraint: String,
    },

    #[error("Query timed out after {ms}ms")]
    Timeout { ms: u64 },

    #[error("Database operation failed")]
    Operational(#[source] sqlx::Error),
}

impl From<sqlx::Error> for DbError {
    fn from(e: sqlx::Error) -> Self {
        match &e {
            sqlx::Error::RowNotFound => DbError::Operational(e),
            sqlx::Error::PoolTimedOut => DbError::PoolExhausted,
            sqlx::Error::Database(db_err) => {
                // 解析 PostgreSQL 特定错误码
                // 23505 = unique_violation
                if db_err.code().map(|c| c == "23505").unwrap_or(false) {
                    DbError::Duplicate {
                        table: db_err.table()
                            .unwrap_or("unknown")
                            .to_string(),
                        constraint: db_err.constraint()
                            .unwrap_or("unknown")
                            .to_string(),
                    }
                } else {
                    DbError::Operational(e)
                }
            }
            _ => DbError::Operational(e),
        }
    }
}

// 数据库仓库方法
impl UserRepo {
    pub async fn find_by_id(&self, id: u64) -> Result<UserRow, DbError> {
        sqlx::query_as!(
            UserRow,
            "SELECT * FROM users WHERE id = $1",
            id as i64
        )
        .fetch_one(&self.pool)
        .await
        .map_err(|e| match e {
            sqlx::Error::RowNotFound => DbError::NotFound {
                table: "users".into(),
                key: "id".into(),
                value: id.to_string(),
            },
            other => other.into(),
        })
    }

    pub async fn create(&self, user: NewUser) -> Result<UserRow, DbError> {
        sqlx::query_as!(
            UserRow,
            "INSERT INTO users (email, name) VALUES ($1, $2)
             RETURNING *",
            user.email, user.name
        )
        .fetch_one(&self.pool)
        .await
        .map_err(Into::into)  // sqlx::Error → DbError
    }
}

Step 2: 服务层错误

crates/service/src/error.rs
use thiserror::Error;
use crate::db::DbError;

#[derive(Error, Debug)]
pub enum ServiceError {
    // 业务领域错误——用领域语言表达
    #[error("User {id} not found")]
    UserNotFound { id: u64 },

    #[error("Email {email} already registered")]
    DuplicateEmail { email: String },

    #[error("Invalid email format: {email}")]
    InvalidEmail { email: String },

    #[error("Cannot delete user {id}: has active orders")]
    UserHasOrders { id: u64 },

    // 基础设施故障——对业务屏蔽细节
    #[error("Service temporarily unavailable")]
    Unavailable,

    #[error("Internal error")]
    Internal,
}

// 关键:显式转换,不泄露 DB 细节
impl From<DbError> for ServiceError {
    fn from(e: DbError) -> Self {
        match e {
            DbError::NotFound { table, value, .. }
                if table == "users" =>
            {
                ServiceError::UserNotFound {
                    id: value.parse().unwrap_or(0),
                }
            }
            DbError::Duplicate { constraint, .. }
                if constraint.contains("email") =>
            {
                // 我们没有 email 信息——需要调用者提供
                ServiceError::DuplicateEmail {
                    email: "unknown".into(),
                }
            }
            DbError::PoolExhausted | DbError::Timeout { .. } => {
                ServiceError::Unavailable
            }
            _ => ServiceError::Internal,
        }
    }
}

// 服务实现
impl UserService {
    pub async fn get_user(&self, id: u64) -> Result<User, ServiceError> {
        let row = self.repo.find_by_id(id).await?;
        Ok(User::from(row))
    }

    pub async fn create_user(
        &self,
        email: String,
        name: String,
    ) -> Result<User, ServiceError> {
        // 业务验证
        if !email.contains('@') {
            return Err(ServiceError::InvalidEmail { email });
        }

        let row = self.repo
            .create(NewUser { email: email.clone(), name })
            .await
            .map_err(|e| match e {
                DbError::Duplicate { constraint, .. }
                    if constraint.contains("email") =>
                {
                    ServiceError::DuplicateEmail { email }
                }
                other => other.into(),
            })?;

        Ok(User::from(row))
    }
}

Step 3: API 层错误

crates/api/src/error.rs
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::Serialize;
use service::ServiceError;
use uuid::Uuid;

#[derive(Debug)]
pub struct ApiError {
    inner: ServiceError,
    request_id: Uuid,
}

impl ApiError {
    pub fn new(inner: ServiceError, request_id: Uuid) -> Self {
        Self { inner, request_id }
    }
}

#[derive(Serialize)]
struct ErrorBody {
    code: String,
    message: String,
    request_id: String,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, code, message) = match &self.inner {
            ServiceError::UserNotFound { .. } => (
                StatusCode::NOT_FOUND,
                "USER_NOT_FOUND",
                "The requested user does not exist",
            ),
            ServiceError::DuplicateEmail { .. } => (
                StatusCode::CONFLICT,
                "DUPLICATE_EMAIL",
                "This email address is already registered",
            ),
            ServiceError::InvalidEmail { .. } => (
                StatusCode::BAD_REQUEST,
                "INVALID_EMAIL",
                "Please provide a valid email address",
            ),
            ServiceError::UserHasOrders { .. } => (
                StatusCode::CONFLICT,
                "USER_HAS_ORDERS",
                "Cannot delete user with active orders",
            ),
            ServiceError::Unavailable => (
                StatusCode::SERVICE_UNAVAILABLE,
                "SERVICE_UNAVAILABLE",
                "Please try again later",
            ),
            ServiceError::Internal => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "INTERNAL_ERROR",
                "An unexpected error occurred",
            ),
        };

        // 📝 日志中记录完整信息(包括 source chain)
        tracing::error!(
            error = %self.inner,
            error_chain = ?self.inner,
            request_id = %self.request_id,
            status = %status.as_u16(),
            "Request failed"
        );

        // 🔒 响应中只返回安全信息
        (status, Json(ErrorBody {
            code: code.to_string(),
            message: message.to_string(),
            request_id: self.request_id.to_string(),
        })).into_response()
    }
}

// Handler 中使用
async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<u64>,
) -> Result<Json<User>, ApiError> {
    let request_id = Uuid::new_v4();

    state.service
        .get_user(id)
        .await
        .map_err(|e| ApiError::new(e, request_id))
        .map(Json)
}

完整数据流

请求: GET /users/42 │ ▼ ┌──────────────────────────────┐ │ API Handler │ │ get_user(id=42) │ │ → Service::get_user(42) │ └──────────────┬───────────────┘ │ ┌──────────┴──────────┐ │ │ Ok(User) Err(ServiceError) │ │ ▼ ▼ ┌──────────┐ ┌──────────────────────────┐ │ 200 JSON │ │ ApiError::into_response() │ │ {user} │ │ ServiceError → HTTP │ └──────────┘ │ + 日志(完整错误链) │ │ + 响应(安全信息) │ └──────────────────────────┘ 成功路径: sqlx::Row → DbError(never) → ServiceError(never) → 200 失败路径: sqlx::Error → DbError::NotFound → ServiceError::UserNotFound → 404

测试错误转换

错误转换测试
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn db_not_found_to_service_user_not_found() {
        let db_err = DbError::NotFound {
            table: "users".into(),
            key: "id".into(),
            value: "42".into(),
        };
        let svc_err: ServiceError = db_err.into();
        assert!(matches!(
            svc_err,
            ServiceError::UserNotFound { id: 42 }
        ));
    }

    #[test]
    fn db_duplicate_email_to_service() {
        let db_err = DbError::Duplicate {
            table: "users".into(),
            constraint: "users_email_key".into(),
        };
        let svc_err: ServiceError = db_err.into();
        assert!(matches!(svc_err, ServiceError::DuplicateEmail { .. }));
    }

    #[test]
    fn db_pool_exhausted_to_unavailable() {
        let db_err = DbError::PoolExhausted;
        let svc_err: ServiceError = db_err.into();
        assert!(matches!(svc_err, ServiceError::Unavailable));
    }

    #[tokio::test]
    async fn api_error_response_format() {
        let svc_err = ServiceError::UserNotFound { id: 42 };
        let api_err = ApiError::new(svc_err, Uuid::nil());

        let response = api_err.into_response();
        assert_eq!(response.status(), 404);
    }
}

错误处理速查表

场景推荐做法crate
库公开 API自定义 enum + thiserrorthiserror
应用 mainanyhow::Resultanyhow
CLI 工具anyhow + contextanyhow
跨层错误显式 From 转换std
HTTP APIServiceError → HTTP status + JSONaxum
异步任务JoinError 处理 + cancellation safetytokio
超时控制tokio::time::timeout + contexttokio
错误链追踪Error::source() + #[from]std