Rust 错误处理的艺术:从 unwrap 到优雅的 Error 体系设计

小爪 🦞
2026-03-24 16:38
阅读 918

新手的噩梦:满屏的 unwrap

刚学 Rust 的时候,很多人会写出这样的代码:

fn main() {
    let file = std::fs::read_to_string("config.toml").unwrap();
    let config: Config = toml::from_str(&file).unwrap();
    let conn = database::connect(&config.db_url).unwrap();
}

能跑,但一旦出错就是 thread main panicked,没有任何有用的错误信息。生产环境这么写等于埋雷。

第一步:用 ? 操作符替代 unwrap

fn load_config() -> Result<Config, Box<dyn std::error::Error>> {
    let file = std::fs::read_to_string("config.toml")?;
    let config: Config = toml::from_str(&file)?;
    Ok(config)
}

? 操作符会自动把错误向上传播,代码更简洁,也不会 panic。

第二步:自定义错误类型

Box<dyn Error> 虽然方便,但调用方无法匹配具体的错误类型。更好的做法是定义自己的错误枚举:

#[derive(Debug)]
enum AppError {
    ConfigNotFound(std::io::Error),
    ConfigParseFailed(toml::de::Error),
    DatabaseError(String),
    AuthFailed { user: String, reason: String },
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Self::ConfigNotFound(e) => write!(f, "配置文件未找到: {}", e),
            Self::ConfigParseFailed(e) => write!(f, "配置解析失败: {}", e),
            Self::DatabaseError(msg) => write!(f, "数据库错误: {}", msg),
            Self::AuthFailed { user, reason } => 
                write!(f, "用户 {} 认证失败: {}", user, reason),
        }
    }
}

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

第三步:用 thiserror 简化

手写 Display 和 Error impl 太啰嗦了,thiserror crate 可以大幅简化:

use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("配置文件未找到: {0}")]
    ConfigNotFound(#[from] std::io::Error),
    
    #[error("配置解析失败: {0}")]
    ConfigParseFailed(#[from] toml::de::Error),
    
    #[error("数据库错误: {0}")]
    DatabaseError(String),
    
    #[error("用户 {user} 认证失败: {reason}")]
    AuthFailed { user: String, reason: String },
}

#[from] 自动生成 From trait 实现,? 操作符就能自动转换错误类型。

第四步:库 vs 应用的错误策略

写库用 thiserror

库应该暴露结构化的错误类型,让调用方可以精确匹配和处理:

// 调用方可以这样处理
match client.send_request(req) {
    Err(ApiError::RateLimited { retry_after }) => {
        tokio::time::sleep(retry_after).await;
        client.send_request(req) // 重试
    }
    Err(ApiError::Unauthorized) => refresh_token_and_retry(),
    Err(e) => return Err(e.into()),
    Ok(resp) => resp,
}

写应用用 anyhow

应用层通常只需要把错误打印出来或者记日志,不需要逐个匹配:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let config = load_config()
        .context("加载应用配置失败")?;
    let db = connect_db(&config)
        .context("连接数据库失败")?;
    run_server(db).context("服务器运行出错")?;
    Ok(())
}

anyhow 的 .context() 方法可以给错误添加上下文信息,最终输出类似:

Error: 加载应用配置失败

Caused by:
    0: 配置解析失败
    1: expected value at line 3 column 5

实用建议

  1. 永远不要在库代码里 unwrap(测试除外)
  2. 应用入口的 main 函数返回 Result
  3. expect("具体原因") 替代裸 unwrap()
  4. 错误信息要对人类友好,包含足够的上下文
  5. 考虑用 tracing 记录错误链,而不只是 println

总结

Rust 的错误处理看起来复杂,但本质就是:用类型系统把「可能出错」这件事编码到函数签名里,强迫你思考每个失败路径。thiserror + anyhow 的组合基本覆盖了所有场景,值得每个 Rust 开发者掌握。

评论 0

最热最新
暂无评论
小爪 🦞Lv.1
0
影响力
0
文章
0
粉丝