FORMA

第五章:错误处理

Rust 将错误明确分为两类:不可恢复的错误与可恢复的错误。通过 panic! 处理前一种,Result 处理后一种,这让程序的错误路径不再是隐式的控制流,而是强类型、必须处理的代码分支。配合 Option 对缺失值的处理以及丰富的组合子与生态库,Rust 形成了安全、清晰的错误处理哲学。

5.1 不可恢复错误与 panic!

当程序遇到无法继续运行的严重问题时,会引发 panic。默认行为是展开线程,清理栈帧并释放资源。对于致命错误,也可以直接 abort

panic! 的使用

调用 panic! 宏会立即中止当前线程:

rust
fn main() {
    panic!("crash and burn");
}

程序将打印错误信息并退出。如果是在多线程环境中,只中止发生 panic 的线程,其它线程仍可继续(但通常会让整个进程受影响)。可以通过 std::panic::set_hook 自定义 panic 行为。

调试栈信息:RUST_BACKTRACE

要查看 panic 时的调用栈,设置环境变量:

bash
RUST_BACKTRACE=1 cargo run

这会打印详尽的回溯信息,辅助定位问题。设为 full 则显示所有栈帧。在发行版中,由于优化可能影响栈帧完整性,建议使用 debug 版本调试。

catch_unwind

Rust 提供了 std::panic::catch_unwind 来捕获 panic,使线程可以在 panic 后继续执行。但它不应被用作常规错误处理,主要用于 FFI 边界或隔离崩溃(例如测试框架、web 服务器的工作线程)。

rust
use std::panic;

let result = panic::catch_unwind(|| {
    panic!("oops!");
});
assert!(result.is_err());

被捕获的 panic 被视为 ResultErr 变体,内部是 Box<dyn Any + Send>,可以尝试 downcast_ref 提取原消息。注意:开启 panic = 'abort' 时,catch_unwind 无效。

abort 的区别

可以在 Cargo.toml 中配置 panic 策略:

toml
[profile.release]
panic = 'abort'
  • 展开(unwind):默认,释放资源,但增加二进制体积。
  • 终止(abort):直接退出程序,不调用析构函数,体积更小,适合嵌入式等场景。

5.2 可恢复错误与 Result

Result<T, E> 是 Rust 处理可能失败的操作的标准方式,强迫调用者显式处理成功和失败两种情况。

定义

rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}

T 是成功时的返回值,E 是错误类型。标准库中许多操作都返回 Result,例如文件操作:

rust
use std::fs::File;
let f = File::open("hello.txt");

处理方式

match 是最基本、最显式的处理方法:

rust
let f = match f {
    Ok(file) => file,
    Err(error) => {
        eprintln!("Problem opening the file: {:?}", error);
        return;
    }
};

原型开发或确信不会出错时,可以使用 unwrapexpect。它们在 Err 时直接 panic,expect 允许附加提示消息:

rust
let f = File::open("hello.txt").unwrap();
let f = File::open("hello.txt").expect("Failed to open hello.txt");

? 运算符

? 是传播错误的便捷语法:如果是 Ok(v),则提取 v;如果是 Err(e),则立即从当前函数返回 Err(From::from(e))(自动进行错误类型转换)。它只能在返回 Result(或 Option)的函数中使用。

rust
use std::fs;
use std::io;

fn read_username() -> Result<String, io::Error> {
    let mut username = String::new();
    fs::File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

当错误类型不同时,? 通过 From trait 将底层错误转换为函数的返回错误类型。这要求返回的错误类型实现了 From<实际错误类型>,通常由库提供。

组合子

Result 提供了一系列组合子方法,可以链式处理,避免显式 match

  • map:将 Ok(T) 映射为 Ok(U),对 Err 无影响。
  • map_err:转换 Err
  • and_then(flat_map):当 Ok 时,调用闭包并返回可能失败的 Result
  • or_else:当 Err 时,调用闭包处理并可能恢复。
  • unwrap_or:提取 Ok 值,若 Err 则返回默认值。
  • unwrap_or_elseErr 时执行闭包产生默认值。
rust
let n = "42".parse::<i32>()
    .map(|x| x * 2)
    .unwrap_or(0);

这些组合子让错误处理流更具表达力。

自定义错误类型与 thiserror

对于库代码,应该定义自己的错误类型,通常用枚举表示不同错误种类,并实现 std::fmt::Displaystd::error::Error

手动实现:

rust
#[derive(Debug)]
enum MyError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MyError::Io(e) => write!(f, "IO error: {e}"),
            MyError::Parse(e) => write!(f, "Parse error: {e}"),
        }
    }
}

impl std::error::Error for MyError {}

然后为 From 实现自动转换,以启用 ?

rust
impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self { MyError::Io(e) }
}
impl From<std::num::ParseIntError> for MyError {
    fn from(e: std::num::ParseIntError) -> Self { MyError::Parse(e) }
}

thiserror crate 则可以大幅简化这个过程:

rust
use thiserror::Error;

#[derive(Error, Debug)]
enum MyError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

该宏会自动生成 DisplayErrorFrom 的实现,是库开发的首选。

5.3 Option 与缺失值处理

Option<T> 表达“可能存在值”的概念,是空值安全的替代品。它也拥有丰富的组合子和与 Result 交互的能力。

常用方法

  • unwrap() / expect():提取 Some 中的值,若是 None 则 panic。
  • mapand_thenfilter:安全地转换 Option
  • ok_or / ok_or_else:将 Option<T> 转为 Result<T, E>,提供错误信息。
rust
let config = Some("value");
let value = config.ok_or("missing config")?; // 返回 Result
  • oror_else:当为 None 时提供回退值或执行闭包。
  • take:获取所有权并留下 None

Result 互相转换

  • OptionResultopt.ok_or(error)opt.ok_or_else(|| error)
  • ResultOptionresult.ok()Ok(v) 变为 Some(v)Err 变为 Noneresult.err() 获取错误。

使用 ? 处理 Option

当函数返回类型是 Option<T> 时,也可以在 Option 值上使用 ?。如果为 None,函数会提前返回 None

rust
fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

这极大地简化了可选值链式处理。

5.4 错误传播最佳实践

Rust 错误处理的实践根据场景分化:应提供明确、可匹配的错误类型;应用程序则更侧重于方便的报告和上下文。

库的错误类型

库应定义清晰区分错误种类的枚举(通常借助 thiserror),让调用者能精确匹配错误原因并决定处理策略。不要直接将上游的 io::Error 或其他 extern 错误暴露给用户,而是包装到自己的错误变体中。同时实现相应的 From,使得 ? 可以无缝转换。

应用程序的错误处理:anyhow

对于二进制程序,通常不需要匹配具体错误种类,而是需要将所有错误向上传播并最终报告给用户并退出。anyhow crate 为此提供了便利的 anyhow::Error 类型,它类似于 Box<dyn Error>,但附带额外的上下文追踪。

rust
use anyhow::{Context, Result};

fn read_config(path: &str) -> Result<String> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read config from {path}"))?;
    Ok(content)
}
  • Result<T>(这里为 anyhow::Result<T>)是 Result<T, anyhow::Error> 的别名。
  • with_context / context 方法为错误附加人类可读的上下文,在最终打印错误链时非常有用。

对于应用入口,通常会有一个 main 返回 anyhow::Result<()>

rust
fn main() -> anyhow::Result<()> {
    let config = read_config("config.toml")?;
    // ...
    Ok(())
}

如果 main 返回 Erranyhow 自动以漂亮格式打印错误,包括所有上下文和原因链。

日志记录与错误上下文

在传播错误的同时,可能还需要记录日志(如 log + env_logger)。可在 map_err 或与 anyhow 的 context 结合时执行。但要注意:不要既记录又传播,通常选择在较高层统一记录并退出,避免重复日志。

map_err 可以转换错误类型的同时添加信息:

rust
let f = File::open("data.txt")
    .map_err(|e| format!("启动失败: {e}"))?;

对于 anyhow,直接使用 .context("...") 就能添加上下文,无需手动进行字符串转换。

综合来说,一个稳健的 Rust 项目会在库层使用 thiserror 定义细粒度错误,在应用层使用 anyhow 快速传播错误并附加上下文,在必要时通过 panic! 处理不可恢复场景。这种分层带来了清晰、安全且易于调试的错误管理体系。

参考文献

资料说明
Error handling官方书第 9 章
thiserror库错误派生
anyhow应用错误传播

相关文章