15.5 Handling Multiple Error Types
Complex applications often face various error scenarios. Rust provides several ways to unify these, allowing you to capture different error types within a single return signature.
15.5.1 Nested Results and Options
Consider this function, which can return Option<Result<i32, ParseIntError>>
:
use std::num::ParseIntError; fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> { vec.first().map(|first| first.parse::<i32>().map(|n| 2 * n)) } fn main() { println!("{:?}", double_first(vec!["42"])); // Some(Ok(84)) println!("{:?}", double_first(vec!["x"])); // Some(Err(ParseIntError(...))) println!("{:?}", double_first(Vec::new())); // None }
If you prefer a Result<Option<T>, E>
, you can use transpose
:
use std::num::ParseIntError; fn double_first(vec: Vec<&str>) -> Result<Option<i32>, ParseIntError> { let opt = vec.first().map(|first| first.parse::<i32>().map(|n| 2 * n)); opt.transpose() } fn main() { println!("{:?}", double_first(vec!["42"])); // Ok(Some(84)) println!("{:?}", double_first(vec!["x"])); // Err(ParseIntError(...)) println!("{:?}", double_first(Vec::new())); // Ok(None) }
15.5.2 Defining a Custom Error Type
To consolidate different error sources, you can define a custom enum or struct:
use std::fmt; type Result<T> = std::result::Result<T, DoubleError>; #[derive(Debug, Clone)] struct DoubleError; impl fmt::Display for DoubleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Invalid first item to double") } } fn double_first(vec: Vec<&str>) -> Result<i32> { vec.first() .ok_or(DoubleError) .and_then(|s| s.parse::<i32>().map_err(|_| DoubleError).map(|i| i * 2)) } fn main() { println!("{:?}", double_first(vec!["42"])); // Ok(84) println!("{:?}", double_first(vec!["x"])); // Err(DoubleError) println!("{:?}", double_first(Vec::new())); // Err(DoubleError) }
15.5.3 Boxing Errors
Alternatively, you can reduce boilerplate by returning a trait object:
use std::error; use std::fmt; type Result<T> = std::result::Result<T, Box<dyn error::Error>>; #[derive(Debug, Clone)] struct EmptyVec; impl fmt::Display for EmptyVec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Invalid first item to double") } } impl error::Error for EmptyVec {} fn double_first(vec: Vec<&str>) -> Result<i32> { vec.first() .ok_or_else(|| EmptyVec.into()) .and_then(|s| s.parse::<i32>().map(|i| i * 2).map_err(|e| e.into())) } fn main() { println!("{:?}", double_first(vec!["42"])); // Ok(84) println!("{:?}", double_first(vec!["x"])); // Err(Box<dyn Error>) println!("{:?}", double_first(Vec::new())); // Err(Box<dyn Error>) }
15.5.4 Automatic Error Conversion with ?
When you use the ?
operator, Rust automatically applies From::from
to convert errors:
use std::error; use std::fmt; use std::num::ParseIntError; type Result<T> = std::result::Result<T, Box<dyn error::Error>>; #[derive(Debug)] struct EmptyVec; impl fmt::Display for EmptyVec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Invalid first item to double") } } impl error::Error for EmptyVec {} fn double_first(vec: Vec<&str>) -> Result<i32> { let first = vec.first().ok_or(EmptyVec)?; let parsed = first.parse::<i32>()?; Ok(parsed * 2) } fn main() { println!("{:?}", double_first(vec!["42"])); // Ok(84) println!("{:?}", double_first(vec!["x"])); // Err(Box<dyn Error>) println!("{:?}", double_first(Vec::new())); // Err(Box<dyn Error>) }
15.5.5 Wrapping Multiple Error Variants
Another strategy is consolidating multiple error types in a single enum:
use std::error; use std::fmt; use std::num::ParseIntError; type Result<T> = std::result::Result<T, DoubleError>; #[derive(Debug)] enum DoubleError { EmptyVec, Parse(ParseIntError), } impl fmt::Display for DoubleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { DoubleError::EmptyVec => write!(f, "Please use a vector with at least one element"), DoubleError::Parse(..) => write!(f, "The provided string could not be parsed as an integer"), } } } impl error::Error for DoubleError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match *self { DoubleError::EmptyVec => None, DoubleError::Parse(ref e) => Some(e), } } } // Convert ParseIntError into DoubleError::Parse impl From<ParseIntError> for DoubleError { fn from(err: ParseIntError) -> DoubleError { DoubleError::Parse(err) } } fn double_first(vec: Vec<&str>) -> Result<i32> { let first = vec.first().ok_or(DoubleError::EmptyVec)?; let parsed = first.parse::<i32>()?; Ok(parsed * 2) } fn main() { println!("{:?}", double_first(vec!["42"])); // Ok(84) println!("{:?}", double_first(vec!["x"])); // Err(Parse(...)) println!("{:?}", double_first(Vec::new())); // Err(EmptyVec) }
Such wrappers keep errors well-defined and traceable, which is crucial for larger projects.