Skip to content

错误处理

Rust 没有异常(Exception),也没有 try/catch。它将错误分为两类,用不同机制处理:

  • 不可恢复错误: 程序逻辑出现严重 bug,使用 panic! 宏终止程序(类似其他语言的未捕获异常)
  • 可恢复错误: 操作可能失败但可以处理,使用 Result<T, E> 枚举(类似其他语言的受检异常)

这种设计迫使开发者在编译期就处理可能的错误,而不是在运行时才发现遗漏。

不可恢复错误: panic!

panic! 用于表示程序遇到了无法继续执行的严重错误。触发时会打印错误信息、展开(unwind)调用栈并终止程序:

rust
fn main() {
    panic!("发生了严重错误");
    // 下面的代码永远不会执行
}

调试 panic 信息

设置 RUST_BACKTRACE 环境变量可以查看完整的调用栈,帮助定位 panic 根源:

bash
# Linux / macOS
RUST_BACKTRACE=1 cargo run          # 显示堆栈跟踪
RUST_BACKTRACE=full cargo run       # 显示完整堆栈跟踪

# Windows PowerShell
$env:RUST_BACKTRACE=1; cargo run    # 显示堆栈跟踪
$env:RUST_BACKTRACE=full; cargo run # 显示完整堆栈跟踪

panic 的行为控制

panic 默认会展开(unwinding)调用栈并逐层清理资源。在 Cargo.toml 中可以改为直接终止,跳过清理过程,使最终二进制文件更小:

toml
[profile.release]
panic = 'abort'

何时使用 panic!: 代码中出现了不应该发生的逻辑错误(bug),如访问越界、违反不可变契约。对于外部输入导致的可预期错误,应使用 Result

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

Result<T, E> 是标准库中用于表示可能失败的操作的枚举,有两个变体:

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

标准库中所有可能失败的函数都返回 Result。例如 File::open() 返回 Result<File, io::Error>: 文件存在时得到 Ok(file),不存在时得到 Err(io::Error)

使用 match 处理

rust
use std::fs::File;

fn main() {
    match File::open("hello.txt") {
        Ok(file) => println!("文件打开成功: {:?}", file),
        Err(e)   => println!("文件打开失败: {}", e),
    }
}

使用 if let 简化

只关心成功的情况时,if letmatch 更简洁:

rust
use std::fs::File;

fn main() {
    if let Ok(file) = File::open("hello.txt") {
        println!("文件打开成功");
        // 在这里使用 file ...
    } else {
        println!("文件打开失败");
    }
}

unwrap 和 expect: 快速但危险

在原型阶段或确定操作不会失败时,可以用这两个快捷方法直接取出 Ok 中的值,失败时直接 panic:

rust
use std::fs::File;

fn main() {
    // unwrap: 失败时 panic,使用默认错误信息
    let f1 = File::open("hello.txt").unwrap();

    // expect: 失败时 panic,使用自定义信息(更易调试,推荐替代 unwrap)
    let f2 = File::open("hello.txt").expect("无法打开配置文件");
}

建议: 生产代码中优先用 expect 而非 unwrap,自定义信息能在 panic 时快速定位问题。更好的做法是用 ? 运算符将错误传播给调用者。

传播错误: ? 运算符

函数遇到错误时往往不想自己处理,而是将错误传播给调用者。先看手动传播的写法:

rust
use std::fs::File;
use std::io::{self, Read};

// 手动传播(繁琐)
fn read_file_manual(path: &str) -> Result<String, io::Error> {
    let mut file = match File::open(path) {
        Ok(f)  => f,
        Err(e) => return Err(e), // 提前返回错误
    };
    let mut content = String::new();
    match file.read_to_string(&mut content) {
        Ok(_)  => Ok(content),
        Err(e) => Err(e),
    }
}

? 运算符将上面的模式自动简化: Ok 时解包取出值,Err 时立即 return Err(e):

rust
// 使用 ? 运算符(简洁)
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;      // 失败自动 return Err
    let mut content = String::new();
    file.read_to_string(&mut content)?;    // 失败自动 return Err
    Ok(content)
}

// 还可以链式调用,进一步简化
fn read_file_chain(path: &str) -> Result<String, io::Error> {
    let mut content = String::new();
    File::open(path)?.read_to_string(&mut content)?;
    Ok(content)
}

? 的限制: 只能用于返回值是 ResultOption 的函数。

? 的自动类型转换

? 最强大之处在于返回错误时会自动调用 From::from 进行类型转换。这解决了函数内可能出现多种错误类型的统一返回问题。最简单的做法是用 Box<dyn Error> 接收任意错误:

Result 返回的错误类型必须保持一致,或者被包含,如果错误范围足够大就可以返回任意错误类型(Box<dyn std::error::Error>),否则就需要自己定义错误类型并实现 From 来支持自动转换为同种错误类型。

rust
use std::error::Error;
use std::fs::File;
use std::io::Read;

fn get_number_from_file() -> Result<i32, Box<dyn Error>> {
    let mut s = String::new();
    File::open("num.txt")?.read_to_string(&mut s)?; // io::Error 自动装箱
    let num: i32 = s.trim().parse()?;               // ParseIntError 自动装箱
    Ok(num)
}

在 main 中使用 ?

默认的 main 函数返回 (),不能使用 ?。让 main 返回 Result 即可解决:

rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = read_file("hello.txt")?; // ? 在 main 中也能用了
    println!("{}", content);
    Ok(())
}

按错误类型分类处理

通过 e.kind() 方法可以匹配 io::ErrorKind 枚举,对不同类型的错误做不同处理:

rust
use std::fs::File;
use std::io::{self, Read};

fn read_text(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

fn main() {
    match read_text("hello.txt") {
        Ok(content) => println!("文件内容:\n{}", content),
        Err(e) => match e.kind() {
            io::ErrorKind::NotFound         => println!("文件不存在,请检查路径"),
            io::ErrorKind::PermissionDenied => println!("没有权限读取该文件"),
            _                               => println!("读取文件时发生错误: {}", e),
        },
    }
}

io::ErrorKind 常用变体:

变体含义
NotFound文件或目录不存在
PermissionDenied权限不足
AlreadyExists文件已存在(创建时)
ConnectionRefused连接被拒绝
TimedOut操作超时
InvalidInput参数无效

自定义错误类型

在实际项目中,通常需要定义自己的错误类型来表示业务逻辑错误,并为 ? 运算符实现自动转换。

下面这个例子展示了如何定义一个包含多种错误类型的 AppError 枚举,并实现 DisplayFrom 将其他错误类型自动转换为 AppError:

rust
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(ParseIntError),
    Custom(String),
}

// 实现 Display,用于格式化输出
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e)     => write!(f, "IO 错误: {}", e),
            AppError::Parse(e)  => write!(f, "解析错误: {}", e),
            AppError::Custom(m) => write!(f, "业务错误: {}", m),
        }
    }
}

// 实现 From,让 ? 运算符自动转换错误类型
impl From<std::io::Error> for AppError {
    // 当 ? 遇到 io::Error 时会自动调用这个函数将其转换为 AppError::Io
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

impl From<ParseIntError> for AppError {
    // 当 ? 遇到 ParseIntError 时会自动调用这个函数将其转换为 AppError::Parse
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

// 现在可以在同一个函数中用 ? 处理不同类型的错误
fn process_config(path: &str) -> Result<i32, AppError> {
    let content = std::fs::read_to_string(path)?; // io::Error → AppError
    let value: i32 = content.trim().parse()?;     // ParseIntError → AppError
    if value < 0 {
        return Err(AppError::Custom("配置值不能为负数".into()));
    }
    Ok(value)
}

第三方库推荐: 实际项目中可用 thiserror 库自动派生 DisplayFrom 实现,大幅减少样板代码。应用层代码可用 anyhow 库提供的 anyhow::Result 快速开发。

Option<T>: 值可能不存在

Option<T> 用于替代其他语言中的 null。Rust 中没有 null,任何可能为空的值都必须用 Option 包装:

rust
enum Option<T> {
    Some(T), // 有值
    None,    // 没有值
}
rust
fn find_user(id: u32) -> Option<String> {
    match id {
        1 => Some("Alice".to_string()),
        2 => Some("Bob".to_string()),
        _ => None, // 用户不存在
    }
}

fn main() {
    // match 处理
    match find_user(1) {
        Some(name) => println!("找到用户: {}", name),
        None       => println!("用户不存在"),
    }

    // if let 简化
    if let Some(name) = find_user(2) {
        println!("找到用户: {}", name);
    }

    // unwrap_or 提供默认值
    let name = find_user(99).unwrap_or("匿名用户".to_string());
    println!("用户名: {}", name); // 匿名用户
}

? 运算符同样适用于 Option: Some 时解包取值,None 时立即 return None:

rust
fn get_first_char(s: &str) -> Option<char> {
    let c = s.chars().next()?; // None 时函数直接返回 None
    Some(c)
}

Option<T> 详细介绍请查看 Option枚举 一章

Result<T, E> 常用方法

查询

方法作用
is_ok()是否是 Ok 变体,返回 bool
is_err()是否是 Err 变体,返回 bool
is_ok_and(f)Ok 且值满足谓词 f(v) 时返回 true
is_err_and(f)Err 且错误满足谓词 f(e) 时返回 true

转换值(不解包)

方法作用
map(f)Ok(v)Ok(f(v))Err 不变
map_err(f)Err(e)Err(f(e))Ok 不变
map_or(default, f)Ok(v) 返回 f(v)Err 返回 default(立即求值)
map_or_else(def_fn, f)Ok(v) 返回 f(v)Err(e) 返回 def_fn(e)(延迟求值)
rust
// map: 变换 Ok 中的值
let n: Result<i32, _> = "42".parse::<i32>().map(|v| v * 2); // Ok(84)

// map_err: 变换 Err 中的错误类型,Ok 不受影响
let result: Result<i32, String> = "abc".parse::<i32>()
    .map_err(|e| format!("解析失败: {e}"));
// Err("解析失败: invalid digit found in string")

链式操作(组合器)

方法作用
and(res)Ok 时返回 resErr 时透传自身错误(res 立即求值)
and_then(f)Ok(v) 时调用 f(v)f 返回 Result);Err 时短路
or(res)Err 时返回 resOk 时透传自身值(res 立即求值)
or_else(f)Err(e) 时调用 f(e)f 返回 Result);Ok 时透传

and_then 等价于函数式编程中的 flatMap,用于串联多个可失败操作:

rust
fn parse_port(s: &str) -> Result<u16, String> {
    s.parse::<i32>()
        .map_err(|e| e.to_string())       // 统一错误类型
        .and_then(|n| {                   // 进一步验证
            if (1..=65535).contains(&n) {
                Ok(n as u16)
            } else {
                Err(format!("端口号超出范围: {n}"))
            }
        })
}

取出值

方法作用
unwrap()取出 Ok 中的值;Err 时 panic
expect(msg)unwrap,但 panic 时显示自定义 msg 和错误内容
unwrap_or(def)Ok 取值;Err 时返回 def
unwrap_or_else(f)Err(e) 时执行闭包 f(e),根据错误生成默认值(延迟求值)
unwrap_or_default()Ok 取值;Err 时返回类型默认值(需实现 Default

调试辅助

inspect / inspect_err 以不可变引用查看内部值,不消耗 Result,用于在链式调用中插入日志:

方法作用
inspect(f)Ok 时以 &v 调用 f,原样返回(不消耗)
inspect_err(f)Err 时以 &e 调用 f,原样返回(不消耗)
rust
let result = "abc".parse::<i32>()
    .inspect(|v| println!("解析成功: {v}"))
    .inspect_err(|e| eprintln!("解析失败: {e}")); // 打印错误但不消耗
// 继续使用 result ...

结构转换

方法作用
flatten()Result<Result<T, E>, E>Result<T, E>(去掉一层嵌套)
transpose()Result<Option<T>, E>Option<Result<T, E>>

Option 与 Result 对比

对比项Option<T>Result<T, E>
用途值可能存在或不存在操作可能成功或失败
成功Some(T)Ok(T)
失败None(无额外信息)Err(E)(包含错误原因)
典型场景查找、可选字段、默认值文件操作、网络请求、解析
转换为对方.ok_or(err)Result.ok()Option

Option → Result

当你有一个可选值,但如果它不存在就想当作一个错误上报时使用。

  • ok_or(err): 如果是 None,则返回 Err(err);如果是 Some(v),则返回 Ok(v)
  • ok_or_else(f): 只有在 None 时才执行闭包 f() 计算错误消息,性能更好(延迟求值)
rust
let opt: Option<i32> = None;
let res = opt.ok_or("错误: 值不存在");         // Err("错误: 值不存在")
let res = opt.ok_or_else(|| format!("未找到")); // 延迟求值,性能更好

Result → Option

当你只关心成功的值,想忽略具体的错误原因时使用

  • ok(): 把 Ok(v) 变成 Some(v),把 Err(e) 直接丢弃变成 None
  • err(): 反过来,把错误提取出来变成 Some(e),成功的值反而变 None
rust
let res: Result<i32, &str> = Err("IO Error");
let opt = res.ok();   // None(错误被丢弃)
let err = res.err();  // Some("IO Error")(取出错误)

总结

场景推荐方式说明
程序逻辑 bugpanic!("原因")终止程序,用于不应该发生的情况
操作可能失败返回 Result<T, E>强制调用者处理错误
在函数内传播错误? 运算符失败时自动 return Err,成功取值
快速原型 / 测试.expect("原因")失败时 panic,但有清晰的错误信息
值可能不存在Option<T>Some / None 替代 null
变换 Ok 中的值.map(f)不解包直接变换,保持错误类型不变
变换错误类型.map_err(f)统一多种错误类型,常与 ? 配合
链式可失败操作.and_then(f)等价 flatMap,串联多步操作
错误时尝试恢复.or_else(f)Err 时执行备用逻辑
链式调用中插入日志.inspect / .inspect_err调试时查看中间值,不打断链式
按错误类型分别处理e.kind()匹配 io::ErrorKind 具体变体
多种错误类型统一返回Box<dyn Error>最简单,适合快速开发
自定义错误类型实现 Display + From配合 ? 实现自动转换,适合生产

基于 MIT 协议发布