Rust 错误处理

`Result`、`?` 运算符、自定义错误、`anyhow` 与 `thiserror`。

Intermediate
Key Points可恢复错误用 `Result`,不可恢复用 `panic!`;`?` 自动向上传播。

panic 与 Result

SituationUse
文件不存在Result
网络超时Result
参数校验失败Result
数组越界(bug)panic!(自动)
配置缺失导致无法启动panic! 或 expect
Rust
fn main() {
    let v = vec![1, 2, 3];
    // v[10];              // panic: index out of bounds

    let r: Result<i32, String> = Err("出错了".into());
    match r {
        Ok(n) => println!("成功 {}", n),
        Err(e) => println!("失败 {}", e),
    }
}
Output
失败 出错了

? 运算符

Rust
use std::fs;
use std::io;

fn read_first_line(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?;      // 出错就返回
    let line = content.lines().next().unwrap_or("").to_string();
    Ok(line)
}

fn main() {
    match read_first_line("nope.txt") {
        Ok(l) => println!("第一行:{}", l),
        Err(e) => println!("错误:{}", e),
    }
}
Output
错误:No such file or directory (os error 2)

? means "on error return Err(From::from(e)), else unwrap". It also converts the error type via From, so the function's error type only needs a matching From impl.

自定义错误

Rust
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    NotFound(String),
    Invalid(String),
    Io(std::io::Error),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::NotFound(s) => write!(f, "找不到:{}", s),
            AppError::Invalid(s) => write!(f, "非法输入:{}", s),
            AppError::Io(e) => write!(f, "IO 错误:{}", e),
        }
    }
}

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

// 让 ? 能自动转换
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

fn load(path: &str) -> Result<String, AppError> {
    let s = std::fs::read_to_string(path)?;   // io::Error 自动转成 AppError
    if s.is_empty() {
        return Err(AppError::Invalid("文件为空".into()));
    }
    Ok(s)
}

thiserror 与 anyhow

TOML
[dependencies]
thiserror = "2"
anyhow = "1"
Rust
use thiserror::Error;

// 库:用 thiserror,调用方需要匹配具体错误
#[derive(Error, Debug)]
pub enum DbError {
    #[error("连接失败:{0}")]
    Connect(String),

    #[error("记录 {id} 不存在")]
    NotFound { id: u64 },

    #[error(transparent)]
    Io(#[from] std::io::Error),
}
Rust
use anyhow::{Context, Result};

// 应用:用 anyhow,不关心具体类型,只要能打印和溯源
fn run() -> Result<()> {
    let cfg = std::fs::read_to_string("config.toml")
        .context("读取配置文件失败")?;

    let port: u16 = cfg.trim().parse()
        .context("端口不是合法数字")?;

    println!("端口 {}", port);
    Ok(())
}

fn main() {
    if let Err(e) = run() {
        eprintln!("错误:{:#}", e);      // {:#} 打印完整因果链
    }
}
Output
错误:读取配置文件失败: No such file or directory (os error 2)
CrateForError type
thiserror库 / 公共 API自定义枚举,精确
anyhow应用 / 二进制anyhow::Error,统一
The rule is simple: **thiserror for libraries, anyhow for applications**. A library returning anyhow::Error robs callers of the ability to match on error kinds.

unwrap 家族

Rust
let s = "42";

// 只在确定不会失败时用(测试、常量)
let n: i32 = s.parse().unwrap();

// 更好:失败时给出上下文
let n: i32 = s.parse().expect("字面量 42 应该能解析");

// 有默认值
let n: i32 = "x".parse().unwrap_or(0);
let n: i32 = "x".parse().unwrap_or_else(|_| 42);

println!("{} {} {}", n, n, n);
Output
42 42 42

unwrap in production is an assertion that it cannot fail. Whenever input is external (files, network, users), avoid it — it turns a recoverable error into a crash.

Friends & Partners