Rust错误处理:Result/Option模式详解
2025.08.20
Rust
9 min
3.4k 字
// 目录 · contents
前言 错误处理哲学 Option类型 基本使用 Option组合器 Result类型 基本使用 ?操作符 ?操作符的From转换 自定义错误类型 手动实现 使用thiserror(推荐用于库) 使用anyhow(推荐用于应用) thiserror vs anyhow 错误转换模式 实战模式 模式1:错误分层 模式2:收集多个错误 模式3:panic的合理使用 最佳实践总结 总结
前言
Rust没有异常机制,而是使用Result<T, E>和Option<T>类型来处理可恢复错误和缺失值。这种设计强制开发者在编译期处理所有可能的错误路径,避免了运行时未捕获异常导致的崩溃。本文将系统性地介绍Rust错误处理的各种模式和最佳实践。
错误处理哲学
graph TB
ERROR[Rust错误分类] --> RECOV[可恢复错误<br>Result<T, E>]
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 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 () { match find_user (1 ) { Some (user) => println! ("Found: {}" , user.name), None => println! ("User not found" ), } if let Some (user) = find_user (1 ) { println! ("Found: {}" , user.name); } let user = find_user (1 ).unwrap (); let user = find_user (1 ).expect ("User 1 must exist" ); let user = find_user (1 ).unwrap_or (User::default ()); 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 let name : Option <String > = find_user (1 ).map (|u| u.name);let email : Option <String > = find_user (1 ) .and_then (|u| u.email); let user = find_user (1 ).or (find_user (2 ));let admin = find_user (1 ).filter (|u| u.is_admin);let pair : Option <(User, Settings)> = find_user (1 ).zip (find_settings (1 ));fn get_user_email (id: u64 ) -> Option <String > { let user = find_user (id)?; let profile = user.profile ()?; Some (profile.email.clone ()) }
Result类型
1 2 3 4 5 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 () { match read_config ("config.toml" ) { Ok (content) => println! ("Config: {}" , content), Err (e) => eprintln! ("Error: {}" , e), } let config = read_config ("config.toml" ).unwrap (); let config = read_config ("config.toml" ).expect ("Failed to read config" ); let config = read_config ("config.toml" ) .unwrap_or_else (|_| String ::from ("default config" )); 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)?; 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 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)?; let number : i32 = content.trim ().parse ()?; 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 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] 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), #[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)?; let parsed : Value = serde_json::from_str (&data)?; 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 use anyhow::{anyhow, bail, Context, Result };async fn fetch_and_process (url: &str ) -> Result <Data> { let response = reqwest::get (url).await .context ("Failed to fetch data from API" )?; if !response.status ().is_success () { bail!("API returned status: {}" , response.status ()); } let data : Data = response.json ().await .context ("Failed to parse response as JSON" )?; if data.items.is_empty () { return Err (anyhow!("No items found in response" )); } Ok (data) }fn main () -> Result <()> { let config = read_config ("config.toml" ) .context ("Failed to read configuration" )?; start_server (&config) .context ("Failed to start server" )?; Ok (()) }
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 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" ));let result : Result <i32 , String > = Ok (42 );let option : Option <i32 > = result.ok (); let result : Result <i32 , String > = Err ("error" .to_string ());let option : Option <i32 > = result.ok (); 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), }#[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) } }fn parse_numbers (inputs: &[&str ]) -> Result <Vec <i32 >, ParseIntError> { inputs.iter () .map (|s| s.parse::<i32 >()) .collect () }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 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" ), } }#[cfg(test)] mod tests { #[test] fn test_parse () { let result = parse ("valid input" ).unwrap (); assert_eq! (result, expected); } }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" ); }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<T>"]
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 let _ = do_something (); let file = File::open (path) .with_context (|| format! ("Failed to open config file: {}" , path))?;#[derive(Error, Debug)] pub enum LibError { #[error("Invalid input: {0}" )] InvalidInput (String ), }fn main () -> anyhow::Result <()> { Ok (()) }
总结
Rust错误处理的核心要点:
Result/Option 替代异常,强制编译时处理所有错误路径
?操作符 是错误传播的语法糖,支持自动From转换
thiserror 用于库开发,定义精确的错误类型枚举
anyhow 用于应用开发,提供统一的错误类型和上下文信息
错误分层 :每层定义自己的错误类型,向上转换
panic用于Bug :不可恢复的编程错误才用panic,不要用于正常错误流
Rust的错误处理模式看似繁琐,但它确保了每个可能的错误路径都被处理,这是构建可靠软件的基础。一旦习惯了这种模式,你会发现它比try-catch更加清晰和可控。