Rust 错误处理
`Result`、`?` 运算符、自定义错误、`anyhow` 与 `thiserror`。
Key Points可恢复错误用 `Result`,不可恢复用 `panic!`;`?` 自动向上传播。
panic 与 Result
| Situation | Use |
|---|---|
| 文件不存在 | 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)| Crate | For | Error 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 42unwrap 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.
panic 与 Result
| 情况 | 用 |
|---|---|
| 文件不存在 | 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)? 等价于「出错就 return Err(From::from(e)),成功就取出值」。注意它还会调用 From::from 做错误类型转换——所以函数返回的错误类型只要实现了 From<原错误> 就能用。
自定义错误
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)| 库 | 适用 | 错误类型 |
|---|---|---|
thiserror | 库 / 公共 API | 自定义枚举,精确 |
anyhow | 应用 / 二进制 | anyhow::Error,统一 |
规则很简单:**库用
thiserror,应用用 anyhow**。库如果返回 anyhow::Error,调用方就没法用 match 区分错误类型了——这是对使用者的伤害。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 42unwrap 在生产代码里是「我确信不会失败」的断言。只要输入来自外部(文件、网络、用户),就不要用 unwrap——它会把一个可恢复错误变成进程崩溃。