15.6 Handling Multiple Error Types
Complex applications often encounter various error kinds. Rust lets you unify these diverse errors under one Result
type, which can be achieved through different strategies.
15.6.1 Results and Options Embedded in Each Other
In the following example, the function returns Option<Result<i32, ParseIntError>>
, allowing for both an empty vector (None
) and a parse error (Err
):
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 need to return a Result<Option<T>, E>
instead, 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.6.2 Defining a Custom Error Type
For unifying multiple error cases, you can define a custom type:
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.6.3 Boxing Errors
For functions that need to handle different error types with minimal boilerplate, you can return a trait object, Box<dyn std::error::Error>
:
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.6.4 Other Uses of ?
The ?
operator automatically performs error conversions via From::from
:
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.6.5 Wrapping Errors
Another approach is to wrap multiple error variants 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) }
This strategy maintains explicit error categories within one custom type, aiding clarity and debugging.