Skip to content
Published at:

07. Error Handling — 错误处理

错误处理是程序设计中最容易被低估的挑战。我们大多数人希望错误「最好不要发生」——它们打乱控制流,让代码膨胀,而且难以测试。但现实世界的程序必须应对文件不存在、网络断开、输入格式非法等种种意外。Rust 的错误处理策略在编译时强制你正视这些可能——代码运行之前,你必须先想好错误该怎么办。

Rust 将错误分为两大类:

类别类型用途默认行为
不可恢复的错误panic!Bug、不变量违反栈展开,线程退出
可恢复的错误Result<T, E>外部因素引起的失败编译器强制处理

一条好经验:不要 panic。把 panic 留给那些「绝对不该发生」的情况——比如索引越界、除零、违反关键的内部不变量。对于文件打开失败、网络超时、用户输入非法等可能发生的情况,永远用 Result

panic!:不可恢复的错误

当程序遇到无法继续的致命错误时,会触发 panic:

rust
// 显式触发 panic
panic!("something went terribly wrong: {}", detail);

// 自动 panic 的场景
let v = vec![1, 2, 3];
let x = v[99];              // 索引越界 → panic
let y = 1 / 0;              // 整数除零 → panic
let z = result.unwrap();    // 对 Err 调用 unwrap → panic
let z = result.expect("msg"); // 对 Err 调用 expect → panic
assert!(false);             // 断言失败 → panic

栈展开

panic 的默认行为是栈展开(stack unwinding):从 panic 点反向遍历调用栈,依次运行每个栈帧中局部变量的 drop 方法——释放资源、关闭文件、归还锁——然后线程退出:

rust
fn pirate_share(total: u64, crew_size: usize) -> u64 {
    let half = total / 2;
    half / crew_size as u64  // 如果 crew_size == 0,这里 panic
}

fn main() {
    let share = pirate_share(100, 0);
    // 不会执行到这里——pirate_share 中的除零导致 panic
    // 栈展开会释放 main 中所有已初始化的局部变量
}

panic 是安全的——它不会违反 Rust 的内存安全保证。未初始化的变量不会被 drop,已初始化的变量按正确顺序释放。panic 是每线程的:一个线程 panic 不影响其他线程继续运行。

可以用 std::panic::catch_unwind() 捕获栈展开:

rust
use std::panic;

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

catch_unwind 主要用于 FFI 边界(C 代码不能处理 Rust 的 panic)和测试框架。不应作为常规错误处理机制。

Abort

在某些环境中(嵌入式、不希望展开),可以编译为 abort 模式:

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

或者在编译命令中:rustc -C panic=abort。发生 panic 时程序立即终止,不执行 drop。注意:两次连续 panic(如在 drop 中再次 panic)无论配置如何都会 abort。

Result<T, E>:可恢复的错误

Result 是 Rust 错误处理的核心类型:

rust
enum Result<T, E> {
    Ok(T),    // 成功,包含成功值
    Err(E),   // 失败,包含错误值
}

任何可能失败的操作都应返回 Result

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

fn read_config(path: &str) -> Result<String, io::Error> {
    // File::open 返回 Result<File, io::Error>
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

Rust 要求你处理每一个 Result 值——编译器会对未使用的 Result 发出警告。这确保了错误不会被静默忽略。

处理 Result

最基本的方式是 match

rust
match read_config("config.toml") {
    Ok(contents) => println!("Config: {}", contents),
    Err(e) => eprintln!("Failed to read config: {}", e),
}

Result 提供了丰富的辅助方法:

方法说明
is_ok()如果是 Ok 返回 true
is_err()如果是 Err 返回 true
ok()Result<T, E>Option<T>
err()Result<T, E>Option<E>
unwrap()获取 Ok 值,否则 panic
expect(msg)获取 Ok 值,否则 panic(带消息)
unwrap_or(default)获取 Ok 值或默认值
unwrap_or_else(f)获取 Ok 值或执行闭包产生默认值
as_ref()&Result<T, E>Result<&T, &E>
as_mut()&mut Result<T, E>Result<&mut T, &mut E>

使用示例:

rust
let result: Result<i32, &str> = Ok(42);

// unwrap_or:提供回退值
let value = result.unwrap_or(0);  // Ok 时返回 42,Err 时返回 0

// unwrap_or_else:惰性计算回退值
let value = result.unwrap_or_else(|e| {
    eprintln!("Using default due to: {}", e);
    0
});

// as_ref:在不消费 Result 的情况下借用内部值
let r = result.as_ref();  // r: Result<&i32, &&str>
match r {
    Ok(n) => println!("Got: {}", n),
    Err(e) => println!("Error: {}", e),
}
// result 仍可用

类型别名

标准库中的许多模块为 Result 定义了类型的别名,避免重复写错误类型:

rust
// std::io::Result 的定义
type Result<T> = std::result::Result<T, std::io::Error>;

// 使用
fn open_config() -> io::Result<String> {
    // 等价于 Result<String, io::Error>
    let mut f = File::open("config.toml")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

打印错误

错误类型通常实现了 DisplayDebug

rust
let err = File::open("nonexistent.txt").unwrap_err();

// {} 输出简洁的错误消息
println!("Error: {}", err);
// Error: No such file or directory (os error 2)

// {:?} 输出详细的调试信息
println!("Error: {:?}", err);
// Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

// to_string() 将错误转为 String
let msg = err.to_string();

标准错误类型可以链式追溯根本原因:

rust
use std::error::Error;

fn print_error(mut err: &dyn Error) {
    eprintln!("Error: {}", err);
    // 遍历错误链——每一层是原始错误的「源头」
    while let Some(source) = err.source() {
        eprintln!("Caused by: {}", source);
        err = source;
    }
}

? 运算符:错误传播

? 是 Rust 错误处理中最常用的运算符。它让错误传播变得简洁:

rust
fn process() -> Result<String, io::Error> {
    let mut f1 = File::open("input.txt")?;    // 出错时立即返回 Err
    let mut f2 = File::open("output.txt")?;   // 同上
    let mut data = String::new();
    f1.read_to_string(&mut data)?;            // 同上
    write!(f2, "{}", data)?;                  // write! 返回 Result
    Ok(data)
}

? 的展开等价于:

rust
let mut f1 = match File::open("input.txt") {
    Ok(file) => file,
    Err(e) => return Err(e.into()),  // .into() 做自动错误类型转换
};

? 不只用于 Result,也可用于 Option

rust
fn first_even(nums: &[i32]) -> Option<&i32> {
    for n in nums {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    // ? 在此上下文中对 None 返回 None
    Some(nums.first()?)  // 如果 first() 返回 None,则传播 None
}

关键的约束:? 只能用在返回 Result(或 Option)的函数中,且错误类型必须兼容(通过 From Trait 自动转换)。

处理多种错误类型

函数可能产生多种类型的错误。以下函数既要处理 IO 错误,又要处理整数解析错误:

rust
use std::num::ParseIntError;

fn read_numbers(path: &str) -> Result<Vec<i32>, /* 什么类型? */> {
    let content = std::fs::read_to_string(path)?;  // io::Error
    let mut nums = Vec::new();
    for line in content.lines() {
        let n: i32 = line.trim().parse()?;  // ParseIntError
        nums.push(n);
    }
    Ok(nums)
}

这会产生编译错误——? 尝试将 ParseIntError 转换为 io::Error,但 From trait 没有对应的实现。

方案一:自定义错误类型

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 trait 让 ? 自动转换
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) }
}

fn read_numbers(path: &str) -> Result<Vec<i32>, MyError> {
    let content = std::fs::read_to_string(path)?;   // io::Error → MyError::Io
    let mut nums = Vec::new();
    for line in content.lines() {
        let n: i32 = line.trim().parse()?;           // ParseIntError → MyError::Parse
        nums.push(n);
    }
    Ok(nums)
}

方案二:Box<dyn Error> 通用错误类型

更简单的方法——擦除具体的错误类型:

rust
type GenericResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;

fn read_numbers(path: &str) -> GenericResult<Vec<i32>> {
    let content = std::fs::read_to_string(path)?;
    let mut nums = Vec::new();
    for line in content.lines() {
        let n: i32 = line.trim().parse()?;
        nums.push(n);
    }
    Ok(nums)
}

如果需要按类型分派处理,可以用 downcast_ref

rust
fn handle_error(err: &dyn std::error::Error) {
    if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
        eprintln!("IO error kind: {:?}", io_err.kind());
    } else if let Some(parse_err) = err.downcast_ref::<std::num::ParseIntError>() {
        eprintln!("Parse error, check the input format");
    }
}

方案三:用 thiserror

thiserror 库通过派生宏消除了样板代码:

rust
use thiserror::Error;

#[derive(Error, Debug)]
enum MyError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),       // #[from] 自动生成 From 实现
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
    #[error("Config key '{key}' not found")]
    MissingKey { key: String },        // 结构化错误变体
}

#[error("...")] 指定 Display 的格式字符串,#[from] 自动生成 From<T> 实现。

自定义错误类型的最佳实践

一个完整的自定义错误类型:

rust
use std::fmt;

#[derive(Debug)]
struct JsonError {
    message: String,
    line: usize,
    column: usize,
}

// 错误必须能够显示
impl fmt::Display for JsonError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} (line {}, column {})", self.message, self.line, self.column)
    }
}

// 实现 Error trait(默认方法通常就够用)
impl std::error::Error for JsonError {}

// 便捷构造方法
impl JsonError {
    fn new(message: &str, line: usize, column: usize) -> Self {
        JsonError {
            message: message.to_string(),
            line,
            column,
        }
    }
}

处理「不可能」发生的错误

有时你「知道」某个操作不会失败——比如刚检查了键的存在就去取值。在这种情况下 unwrap() 是合理的:

rust
let mut map = HashMap::new();
map.insert("key", 42);

// 刚插入的键必然存在——unwrap 是安全的
let val = map.get("key").unwrap();

// 或者用 expect 附带你的推理
let val = map.get("key")
    .expect("'key' was just inserted, so it must exist");

但要保持警惕——你以为的「不可能」可能在意料之外的情况下发生。unwrap() 在代码审查中是一个信号:确认你是否真的证明了这个操作不会失败。

忽略错误

有时错误确实无关紧要。用 let _ = 来显式忽略:

rust
// writeln! 返回 io::Result,但在日志场景中失败了也只能忽略
let _ = writeln!(stderr(), "Failed to process item: {}", item_id);

let _ = 比直接调用而不赋值要好——它明确表达了「我知道这里可能出错,但选择忽略」。

main() 中的错误处理

main() 函数也可以返回 Result

rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = read_config()?;
    let server = Server::new(config)?;
    server.run()?;
    Ok(())
}

程序结束时,如果 main 返回 Err,Rust 会打印错误并退出(退出码非零)。这种方式让 ? 贯穿整个程序。

另一种方式是 main 中手动处理:

rust
fn main() {
    if let Err(e) = run_app() {
        eprintln!("Fatal error: {}", e);
        if let Some(source) = e.source() {
            eprintln!("Caused by: {}", source);
        }
        std::process::exit(1);
    }
}

fn run_app() -> Result<(), Box<dyn std::error::Error>> {
    // 所有 ? 在这里自然流动
    Ok(())
}

何时 panic,何时用 Result

场景建议
原型开发unwrap() / expect() 可接受
测试代码unwrap() —— 测试失败 = bug
不变量违反(逻辑错误)panic!
公共 API永不 panic,一定返回 Result
外部输入(文件、网络、用户)Result
可以 graceful 降级的失败Result
后续代码在不变量满足时才有意义panic!

核心原则:边界处用 Result,内部不变量用 panic。库的作者不应 panic(让调用者决定如何处理失败),应用程序的作者可以更自由地选择。

为什么是 Result

对比其他语言的错误处理方式:

语言机制问题
C返回码 + errno容易忽略,不强制检查
C++异常隐式控制流,性能开销,异常安全难保证
Java受检异常样板代码多,lambda 不友好
Go多返回值 (T, error)可能忽略,没有 ? 的便利
Python异常动态类型,运行前不检查

Rust 的 Result + ? 组合:

  • 错误路径在代码中可见。 每个 ? 标记了一个可能的返回点。没有隐藏的控制流。
  • 编译器强制你不忽略错误。 未使用的 Result 产生警告。不会静默吞掉错误。
  • 零运行时开销。 Result 是一个简单的枚举,没有异常表、没有栈展开逻辑。
  • 明确区分可恢复与不可恢复。 Result vs panic! 的二分类在设计阶段就迫使你做出决策。

代价是:你会在 Rust 中花更多时间思考和设计错误处理策略。Rust 社区认为这是值得的投资——它在编译时消灭了一整类生产环境 bug。

小结

  • 两类错误panic! 用于不可恢复的 Bug(不变量违反),Result<T, E> 用于可恢复的外部失败。默认不 panic。
  • panic! 触发栈展开:释放局部变量后线程退出。安全但不应作为常规错误处理。catch_unwind 用于 FFI 边界。
  • Result 枚举Ok(T) / Err(E)。编译器强制处理——未使用会产生警告。
  • ? 运算符match + return Err 的语法糖。传播错误到调用者,同时通过 From 自动转换错误类型。
  • 多种错误类型处理:自定义枚举 + From 实现、Box<dyn Error>、或 thiserror 库。downcast_ref 可在擦除后恢复具体类型。
  • main() 可返回 Result:让 ? 贯穿整个程序。错误被自动打印,进程以非零码退出。
  • 每条 ? 可见,每个 Result 必须被使用。 Rust 让错误处理路径成为一等公民,代价是需要在设计阶段投入更多思考。