Rust · #rust#error-handling#result

Rust错误处理:Result/Option模式详解

2025.08.20 Rust 9 min 3.4k
// 目录 · contents

前言

Rust没有异常机制,而是使用Result<T, E>Option<T>类型来处理可恢复错误和缺失值。这种设计强制开发者在编译期处理所有可能的错误路径,避免了运行时未捕获异常导致的崩溃。本文将系统性地介绍Rust错误处理的各种模式和最佳实践。

错误处理哲学

graph TB
    ERROR[Rust错误分类] --> RECOV[可恢复错误<br>Result&lt;T, E&gt;]
    ERROR --> UNRECOV[不可恢复错误<br>panic!]

    RECOV --> FILE[文件未找到]
    RECOV --> NET[网络超时]
    RECOV --> PARSE[解析失败]

    UNRECOV --> BUG[程序Bug]
    UNRECOV --> ASSERT[断言失败]
    UNRECOV --> INDEX[数组越界]

    style RECOV fill:#388e3c,color:#fff
    style UNRECOV fill:#d32f2f,color:#fff

核心原则: - 可恢复错误Result,调用者决定如何处理 - 不可恢复错误panic!,表示程序Bug - 缺失值Option,表示值可能不存在

Option类型

1
2
3
4
5
// Option定义
enum Option<T> {
Some(T), // 有值
None, // 无值
}

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn find_user(id: u64) -> Option<User> {
let users = get_users();
users.into_iter().find(|u| u.id == id)
}

fn main() {
// 1. match
match find_user(1) {
Some(user) => println!("Found: {}", user.name),
None => println!("User not found"),
}

// 2. if let
if let Some(user) = find_user(1) {
println!("Found: {}", user.name);
}

// 3. unwrap (如果None则panic,仅用于确定有值或原型)
let user = find_user(1).unwrap();

// 4. expect (同unwrap,但有自定义消息)
let user = find_user(1).expect("User 1 must exist");

// 5. unwrap_or (提供默认值)
let user = find_user(1).unwrap_or(User::default());

// 6. unwrap_or_else (惰性默认值)
let user = find_user(1).unwrap_or_else(|| create_default_user());
}

Option组合器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// map: 转换内部值
let name: Option<String> = find_user(1).map(|u| u.name);

// and_then (flatmap): 链式操作
let email: Option<String> = find_user(1)
.and_then(|u| u.email); // email本身也是Option<String>

// or: 提供备选Option
let user = find_user(1).or(find_user(2));

// filter: 条件筛选
let admin = find_user(1).filter(|u| u.is_admin);

// zip: 组合两个Option
let pair: Option<(User, Settings)> = find_user(1).zip(find_settings(1));

// ? 操作符在返回Option的函数中使用
fn get_user_email(id: u64) -> Option<String> {
let user = find_user(id)?; // None则提前返回
let profile = user.profile()?; // None则提前返回
Some(profile.email.clone())
}

Result类型

1
2
3
4
5
// Result定义
enum Result<T, E> {
Ok(T), // 成功
Err(E), // 失败
}

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use std::fs;
use std::io;

fn read_config(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}

fn main() {
// 1. match
match read_config("config.toml") {
Ok(content) => println!("Config: {}", content),
Err(e) => eprintln!("Error: {}", e),
}

// 2. unwrap / expect
let config = read_config("config.toml").unwrap();
let config = read_config("config.toml").expect("Failed to read config");

// 3. unwrap_or_else
let config = read_config("config.toml")
.unwrap_or_else(|_| String::from("default config"));

// 4. if let
if let Ok(config) = read_config("config.toml") {
process_config(&config);
}
}

?操作符

?是Rust错误处理的核心语法糖,自动传播错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::fs::File;
use std::io::{self, Read};

// 没有?操作符
fn read_file_verbose(path: &str) -> Result<String, io::Error> {
let file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}

// 使用?操作符
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // 错误自动return
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}

// 更简洁
fn read_file_short(path: &str) -> Result<String, io::Error> {
std::fs::read_to_string(path)
}
flowchart TB
    CALL["File::open(path)?"] --> CHECK{Result?}
    CHECK -->|Ok(file)| CONTINUE[继续执行<br>得到file]
    CHECK -->|Err(e)| RETURN[自动return Err(e.into())]

    style CONTINUE fill:#388e3c,color:#fff
    style RETURN fill:#d32f2f,color:#fff

?操作符的From转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ?会自动调用From::from进行错误类型转换
use std::num::ParseIntError;

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

impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}

impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}

fn read_and_parse(path: &str) -> Result<i32, AppError> {
let content = std::fs::read_to_string(path)?; // io::Error -> AppError
let number: i32 = content.trim().parse()?; // ParseIntError -> AppError
Ok(number)
}

自定义错误类型

手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum ApiError {
NotFound { resource: String },
Unauthorized { message: String },
Internal { source: Box<dyn Error + Send + Sync> },
Validation { field: String, message: String },
}

impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApiError::NotFound { resource } =>
write!(f, "Resource not found: {}", resource),
ApiError::Unauthorized { message } =>
write!(f, "Unauthorized: {}", message),
ApiError::Internal { source } =>
write!(f, "Internal error: {}", source),
ApiError::Validation { field, message } =>
write!(f, "Validation error on '{}': {}", field, message),
}
}
}

impl Error for ApiError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ApiError::Internal { source } => Some(source.as_ref()),
_ => None,
}
}
}

使用thiserror(推荐用于库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Cargo.toml: thiserror = "2"

use thiserror::Error;

#[derive(Error, Debug)]
enum ApiError {
#[error("Resource not found: {resource}")]
NotFound { resource: String },

#[error("Unauthorized: {message}")]
Unauthorized { message: String },

#[error("Internal error")]
Internal {
#[source] // 自动实现Error::source()
source: Box<dyn std::error::Error + Send + Sync>,
},

#[error("Validation error on '{field}': {message}")]
Validation { field: String, message: String },

#[error("IO error: {0}")]
Io(#[from] std::io::Error), // 自动实现From<io::Error>

#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),

#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
}

// 使用
fn process_request(req: Request) -> Result<Response, ApiError> {
let data = std::fs::read_to_string(&req.path)?; // 自动转为ApiError::Io
let parsed: Value = serde_json::from_str(&data)?; // 自动转为ApiError::Json

if parsed.is_null() {
return Err(ApiError::NotFound {
resource: req.path.clone(),
});
}

Ok(Response::new(parsed))
}

使用anyhow(推荐用于应用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Cargo.toml: anyhow = "1"

use anyhow::{anyhow, bail, Context, Result};

// anyhow::Result<T> = Result<T, anyhow::Error>

async fn fetch_and_process(url: &str) -> Result<Data> {
// context()添加上下文信息
let response = reqwest::get(url).await
.context("Failed to fetch data from API")?;

if !response.status().is_success() {
// bail!宏快速返回错误
bail!("API returned status: {}", response.status());
}

let data: Data = response.json().await
.context("Failed to parse response as JSON")?;

if data.items.is_empty() {
// anyhow!宏创建错误
return Err(anyhow!("No items found in response"));
}

Ok(data)
}

// main函数使用anyhow
fn main() -> Result<()> {
let config = read_config("config.toml")
.context("Failed to read configuration")?;

start_server(&config)
.context("Failed to start server")?;

Ok(())
}

// 输出的错误链:
// Error: Failed to start server
//
// Caused by:
// 0: Failed to bind to port 8080
// 1: Address already in use (os error 98)

thiserror vs anyhow

graph TB
    CHOOSE[选择错误处理库] --> Q{你在写什么?}

    Q -->|库/框架| THISERROR[thiserror<br>定义精确的错误类型<br>调用者可以match]
    Q -->|应用程序| ANYHOW[anyhow<br>统一错误类型<br>快速开发]
    Q -->|两者兼有| BOTH[库用thiserror<br>应用用anyhow]

    THISERROR --> TE_PROS["优点:<br>- 类型安全<br>- 调用者可以模式匹配<br>- 编译时检查"]
    ANYHOW --> AE_PROS["优点:<br>- 简单快速<br>- 自动错误链<br>- context()信息丰富"]

错误转换模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Option -> Result
let value: Option<i32> = Some(42);
let result: Result<i32, &str> = value.ok_or("value is None");

let value: Option<i32> = None;
let result: Result<i32, String> = value.ok_or_else(|| format!("missing value"));

// Result -> Option
let result: Result<i32, String> = Ok(42);
let option: Option<i32> = result.ok(); // Some(42)

let result: Result<i32, String> = Err("error".to_string());
let option: Option<i32> = result.ok(); // None

// Result映射
let result: Result<String, io::Error> = Ok("42".to_string());
let mapped: Result<i32, io::Error> = result.map(|s| s.parse().unwrap());

// 错误映射
let result: Result<i32, ParseIntError> = "42".parse();
let mapped: Result<i32, String> = result.map_err(|e| e.to_string());

实战模式

模式1:错误分层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 数据访问层错误
#[derive(Error, Debug)]
enum DbError {
#[error("Connection failed: {0}")]
Connection(#[source] sqlx::Error),

#[error("Query failed: {0}")]
Query(#[source] sqlx::Error),

#[error("Record not found: {table}.{id}")]
NotFound { table: String, id: String },
}

// 服务层错误(转换底层错误)
#[derive(Error, Debug)]
enum ServiceError {
#[error("User not found: {0}")]
UserNotFound(String),

#[error("Permission denied")]
PermissionDenied,

#[error("Internal error")]
Internal(#[from] DbError),
}

// API层错误(转换为HTTP响应)
#[derive(Error, Debug)]
enum ApiError {
#[error("{0}")]
BadRequest(String),

#[error("{0}")]
NotFound(String),

#[error("Forbidden")]
Forbidden,

#[error("Internal server error")]
InternalError(#[source] Box<dyn Error + Send + Sync>),
}

impl From<ServiceError> for ApiError {
fn from(e: ServiceError) -> Self {
match e {
ServiceError::UserNotFound(msg) => ApiError::NotFound(msg),
ServiceError::PermissionDenied => ApiError::Forbidden,
ServiceError::Internal(e) => ApiError::InternalError(Box::new(e)),
}
}
}
graph TB
    API[API Layer<br>ApiError] --> SERVICE[Service Layer<br>ServiceError]
    SERVICE --> DB[Data Layer<br>DbError]
    DB --> SQLX[sqlx::Error]

    API --> |HTTP 400/404/500| CLIENT[Client]

    style API fill:#1976d2,color:#fff
    style SERVICE fill:#388e3c,color:#fff
    style DB fill:#f57c00,color:#fff

模式2:收集多个错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
fn validate_user(user: &UserInput) -> Result<(), Vec<String>> {
let mut errors = Vec::new();

if user.name.is_empty() {
errors.push("Name is required".to_string());
}
if user.email.is_empty() {
errors.push("Email is required".to_string());
}
if user.age < 18 {
errors.push("Must be at least 18 years old".to_string());
}

if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}

// 收集迭代器中的Result
fn parse_numbers(inputs: &[&str]) -> Result<Vec<i32>, ParseIntError> {
inputs.iter()
.map(|s| s.parse::<i32>())
.collect() // collect可以将Iterator<Result>转为Result<Vec>
}

// 分离成功和失败
fn partition_results(inputs: &[&str]) -> (Vec<i32>, Vec<String>) {
let (oks, errs): (Vec<_>, Vec<_>) = inputs.iter()
.map(|s| s.parse::<i32>())
.partition(Result::is_ok);

let oks: Vec<i32> = oks.into_iter().map(Result::unwrap).collect();
let errs: Vec<String> = errs.into_iter()
.map(|e| e.unwrap_err().to_string())
.collect();

(oks, errs)
}

模式3:panic的合理使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 合理的panic场景

// 1. 不可能发生的情况(unreachable)
fn process(value: &Value) {
match value {
Value::Number(n) => handle_number(n),
Value::String(s) => handle_string(s),
_ => unreachable!("validate() ensures only Number and String"),
}
}

// 2. 测试中
#[cfg(test)]
mod tests {
#[test]
fn test_parse() {
let result = parse("valid input").unwrap(); // 测试中可以unwrap
assert_eq!(result, expected);
}
}

// 3. 初始化(失败则无法继续)
fn main() {
let config = Config::load().expect("Failed to load config - cannot start");
let db = Database::connect(&config.db_url)
.expect("Failed to connect to database - cannot start");
}

// 4. 使用assert!验证不变量
fn binary_search(arr: &[i32], target: i32) -> Option<usize> {
assert!(arr.is_sorted(), "Array must be sorted for binary search");
// ...
}

最佳实践总结

flowchart TB
    START[如何处理错误?] --> Q1{是程序Bug?}
    Q1 -->|是| PANIC["panic! / unreachable!<br>assert!"]
    Q1 -->|否| Q2{值可能不存在?}

    Q2 -->|是| OPTION["Option&lt;T&gt;"]
    Q2 -->|否| Q3{写库还是应用?}

    Q3 -->|库| THISERROR["thiserror<br>定义精确错误枚举"]
    Q3 -->|应用| ANYHOW["anyhow<br>统一错误 + context"]
    Q3 -->|两者| BOTH["库: thiserror<br>应用: anyhow"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 最佳实践示例

// 1. 永远不要忽略错误
let _ = do_something(); // 至少用let _承认你知道这里有Result

// 2. 使用context()提供有意义的错误信息
let file = File::open(path)
.with_context(|| format!("Failed to open config file: {}", path))?;

// 3. 在库中使用thiserror定义明确的错误类型
#[derive(Error, Debug)]
pub enum LibError {
#[error("Invalid input: {0}")]
InvalidInput(String),
// ...
}

// 4. 在应用中使用anyhow简化错误处理
fn main() -> anyhow::Result<()> {
// ...
Ok(())
}

// 5. 不要过度使用unwrap()
// 只在确定不会失败或原型阶段使用

总结

Rust错误处理的核心要点:

  1. Result/Option替代异常,强制编译时处理所有错误路径
  2. ?操作符是错误传播的语法糖,支持自动From转换
  3. thiserror用于库开发,定义精确的错误类型枚举
  4. anyhow用于应用开发,提供统一的错误类型和上下文信息
  5. 错误分层:每层定义自己的错误类型,向上转换
  6. panic用于Bug:不可恢复的编程错误才用panic,不要用于正常错误流

Rust的错误处理模式看似繁琐,但它确保了每个可能的错误路径都被处理,这是构建可靠软件的基础。一旦习惯了这种模式,你会发现它比try-catch更加清晰和可控。

作者 · authorzt
发布 · date2025-08-20
篇幅 · length3.4k 字 · 9 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论