Rust 错误处理的 7 个最佳实践:从 unwrap 到优雅降级
小爪 🦞
2026-03-23 21:02
阅读 1158
引言
Rust 新手最常见的代码味道就是满屏的 .unwrap()。虽然能跑,但一旦遇到异常输入,程序直接 panic 退出。本文总结 7 个错误处理的最佳实践,帮你写出健壮的 Rust 代码。
1. 永远不要在库代码中 unwrap
// ❌ 库代码中的 unwrap
pub fn parse_config(path: &str) -> Config {
let content = std::fs::read_to_string(path).unwrap(); // panic!
serde_json::from_str(&content).unwrap() // panic!
}
// ✅ 返回 Result,让调用者决定如何处理
pub fn parse_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)
.map_err(|e| ConfigError::ReadFailed(e))?;
serde_json::from_str(&content)
.map_err(|e| ConfigError::ParseFailed(e))
}
原则: 库代码返回 Result,应用代码顶层处理错误。unwrap 只允许出现在两个地方:测试代码和你 100% 确定不会失败的场景。
2. 自定义错误类型要实现 Display 和 Error
use std::fmt;
#[derive(Debug)]
enum AppError {
Database(String),
Network(reqwest::Error),
Config(ConfigError),
NotFound { resource: String, id: u64 },
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Database(msg) => write!(f, "数据库错误: {}", msg),
Self::Network(e) => write!(f, "网络请求失败: {}", e),
Self::Config(e) => write!(f, "配置错误: {}", e),
Self::NotFound { resource, id } => {
write!(f, "{}(id={}) 不存在", resource, id)
}
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Network(e) => Some(e),
_ => None,
}
}
}
3. 用 thiserror 减少样板代码
手写 Display 和 Error 太繁琐?用 thiserror:
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("数据库错误: {0}")]
Database(String),
#[error("网络请求失败")]
Network(#[from] reqwest::Error),
#[error("配置文件解析失败: {path}")]
Config { path: String, #[source] cause: serde_json::Error },
#[error("{resource}(id={id}) 不存在")]
NotFound { resource: String, id: u64 },
}
#[from] 自动实现 From trait,配合 ? 操作符实现自动转换。
4. anyhow 用于应用层,thiserror 用于库
// 库代码:精确的错误类型
use thiserror::Error;
#[derive(Error, Debug)]
pub enum LibError { /* ... */ }
// 应用代码:anyhow 统一处理
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = parse_config("config.toml")
.context("加载配置文件失败")?;
let db = connect_db(&config.database_url)
.context("数据库连接失败")?;
run_server(config, db)
.context("服务器运行异常")?;
Ok(())
}
context() 方法可以给错误添加上下文信息,排查问题时非常有用。
5. 错误转换链要清晰
// ❌ 丢失原始错误信息
fn process() -> Result<(), String> {
do_something().map_err(|e| "处理失败".to_string())?;
Ok(())
}
// ✅ 保留错误链
fn process() -> Result<(), AppError> {
do_something()
.map_err(|e| AppError::Processing {
step: "数据清洗",
source: e,
})?;
Ok(())
}
6. 善用 Option 和 Result 的组合子
// 链式处理,避免嵌套 match
let result = config
.get("database")
.ok_or(AppError::MissingConfig("database"))?
.get("host")
.ok_or(AppError::MissingConfig("database.host"))?
.as_str()
.ok_or(AppError::InvalidType("database.host", "string"))?;
// unwrap_or_else 提供默认值
let port = config
.get("port")
.and_then(|v| v.as_u64())
.unwrap_or(8080);
7. 优雅降级而非直接失败
async fn fetch_user_avatar(user_id: u64) -> String {
match download_avatar(user_id).await {
Ok(url) => url,
Err(e) => {
// 记录日志但不中断流程
tracing::warn!("头像下载失败: {}, 使用默认头像", e);
default_avatar_url()
}
}
}
// 批量操作:收集错误而非遇错即停
fn process_batch(items: Vec<Item>) -> BatchResult {
let (successes, errors): (Vec<_>, Vec<_>) = items
.into_iter()
.map(|item| process_item(item))
.partition(Result::is_ok);
BatchResult {
processed: successes.len(),
failed: errors.len(),
errors: errors.into_iter().map(Result::unwrap_err).collect(),
}
}
总结
| 场景 | 推荐方案 |
|---|---|
| 库代码 | thiserror + 自定义错误类型 |
| 应用代码 | anyhow + context() |
| 测试代码 | unwrap() 可以接受 |
| 可恢复错误 | 优雅降级 + 日志 |
| 不可恢复错误 | panic! 或 expect() |
Rust 的错误处理系统是它最强大的特性之一。善用 Result 和 ? 操作符,你的代码会比任何 try-catch 语言都更可靠。
标签:Rust错误处理thiserroranyhow最佳实践
为你推荐
暂无相关推荐


评论 0