Error handling
- Most errors aren’t serious enough to require the program to stop entirely.
- Sometimes, when a function fails, it’s for a reason that you can easily interpret and respond to.
For example, if you try to open a file and that operation fails because the file doesn’t exist, you might want to create the file instead of terminating the process.
Is Option part of error handling
In Rust, an “option” is not technically part of error handling, but it is often used in combination with error handling to represent the possibility of a value being absent or “None.”
- An “option” is a type that represents either Some value or None.
- When a function may not return a value, it can return an Option
where T is the type of the value that might be returned. - If the function succeeds and returns a value, it returns Some(value); otherwise, it returns None.
Error handling in Rust typically uses the Result<T, E> type
- where T is the type of the value that is returned if the operation succeeds
- and E is the type of the error that may occur.
- When an error occurs, a value of type E is returned, and when the operation succeeds, a value of type T is returned.
So while Option and Result are different types in Rust, they are often used in combination to handle situations where a value may or may not be present or when an operation may or may not succeed.
Further information
- Rust 使用 Result 的错误处理方式与 Golang 使用 error 的方式有什么本质区别? - 知乎: dt link
- 错误处理内容和主流方法 - Anatomy In First Rust Programming Class 🦀
- ⭐️Recoverable Errors with Result - The Rust Programming Language
- ✨Generic Data Types - The Rust Programming Language
- ⭐️Error handling: Panic、Option and Result - The Rust Programming Language
rustlings-solutions-5/error_handling at main · gaveen/rustlings-solutions-5
Rustlings
errors1: change Option to Result<T, E>
// errors1.rs // This function refuses to generate text to be printed on a nametag if // you pass it an empty string. It'd be nicer if it explained what the problem // was, instead of just sometimes returning `None`. Thankfully, Rust has a similar // construct to `Option` that can be used to express error conditions. Let's use it! // Execute `rustlings hint errors1` or use the `hint` watch subcommand for a hint. // I AM NOT DONE pub fn generate_nametag_text(name: String) -> Option<String> { if name.is_empty() { // Empty names aren't allowed. None } else { Some(format!("Hi! My name is {}", name)) } } // convert unit tests to main fn main() { fn generates_nametag_text_for_a_nonempty_name() { assert_eq!( generate_nametag_text("Beyoncé".into()), Ok("Hi! My name is Beyoncé".into()) ); } fn explains_why_generating_nametag_text_fails() { assert_eq!( generate_nametag_text("".into()), // Don't change this line Err("`name` was empty; it must be nonempty.".into()) ); } generates_nametag_text_for_a_nonempty_name(); explains_why_generating_nametag_text_fails(); } #[cfg(test)] mod tests { use super::*; #[test] fn generates_nametag_text_for_a_nonempty_name() { assert_eq!( generate_nametag_text("Beyoncé".into()), Ok("Hi! My name is Beyoncé".into()) ); } #[test] fn explains_why_generating_nametag_text_fails() { assert_eq!( generate_nametag_text("".into()), // Don't change this line Err("`name` was empty; it must be nonempty.".into()) ); } }
Hint
Ok and Err are one of the variants of Result, so what the tests are saying
is that generate_nametag_text should return a Result instead of an
Option.
To make this change, you’ll need to:
- update the return type in the function signature to be a Result<String, String> that
could be the variants
Ok(String)andErr(String) - change the body of the function to return
Ok(stuff)where it currently returnsSome(stuff) - change the body of the function to return
Err(error message)where it currently returnsNone
solution: Result<String, String>
pub fn generate_nametag_text(name: String) -> Result<String, String> { if name.is_empty() { // Empty names aren't allowed. Err(format!("`name` was empty; it must be nonempty.")) } else { Ok(format!("Hi! My name is {}", name)) } }
- 这里其实单元测试代码已经指出要报错的内容
- Result<T, E>中,E其实就是各种Err
- Say we’re writing a game where you can buy items with tokens. All items cost 5 tokens, and whenever you purchase items there is a processing fee of 1 token.
- A player of the game will type in how many items they want to buy,
and the
total_costfunction will calculate the total number of tokens. - Since the player typed in the quantity, though, we get it as a string– and they might have typed anything, not just numbers!
Right now, this function isn’t handling the error case at all (and isn’t handling the success case properly either). What we want to do is:
- if we call the
parsefunction on a string that is not a number, that function will return aParseIntError, - and in that case, we want to immediately return that error from our function and not try to multiply and add.
There are at least two ways to implement this that are both correct– but one is a lot shorter!
⭐️errors2: unwrap_err()
// errors2.rs // Say we're writing a game where you can buy items with tokens. All items cost // 5 tokens, and whenever you purchase items there is a processing fee of 1 // token. A player of the game will type in how many items they want to buy, // and the `total_cost` function will calculate the total number of tokens. // Since the player typed in the quantity, though, we get it as a string-- and // they might have typed anything, not just numbers! // Right now, this function isn't handling the error case at all (and isn't // handling the success case properly either). What we want to do is: // if we call the `parse` function on a string that is not a number, that // function will return a `ParseIntError`, and in that case, we want to // immediately return that error from our function and not try to multiply // and add. // There are at least two ways to implement this that are both correct-- but // one is a lot shorter! // Execute `rustlings hint errors2` or use the `hint` watch subcommand for a hint. // I AM NOT DONE use std::num::ParseIntError; pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> { let processing_fee = 1; let cost_per_item = 5; let qty = item_quantity.parse::<i32>(); Ok(qty * cost_per_item + processing_fee) } // convert unit tests to main fn main() { fn item_quantity_is_a_valid_number() { assert_eq!(total_cost("34"), Ok(171)); } fn item_quantity_is_an_invalid_number() { assert_eq!( total_cost("beep boop").unwrap_err().to_string(), "invalid digit found in string" ); } item_quantity_is_a_valid_number(); item_quantity_is_an_invalid_number() } #[cfg(test)] mod tests { use super::*; #[test] fn item_quantity_is_a_valid_number() { assert_eq!(total_cost("34"), Ok(171)); } #[test] fn item_quantity_is_an_invalid_number() { assert_eq!( total_cost("beep boop").unwrap_err().to_string(), "invalid digit found in string" ); } }
Hint
- One way to handle this is using a
matchstatement onitem_quantity.parse::<i32>()where the cases areOk(something)andErr(something). This pattern is very common in Rust, though, so there’s a?operator that does pretty much what you would make that match statement do for you! - Take a look at this section of the
match expressionpart in Error Handling chapter:
solution1: match err to panic! return
pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> { let processing_fee = 1; let cost_per_item = 5; let qty = match item_quantity.parse::<i32>(){ Ok(iqty) => iqty, Err(error) => panic!("Problem parsing the item_quantity: {:?}", error), }; Ok(qty * cost_per_item + processing_fee) }
solution2: match err to early return
pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> { let processing_fee = 1; let cost_per_item = 5; let qty = match item_quantity.parse::<i32>(){ Ok(iqty) => iqty, Err(e) => return Err(e), }; Ok(qty * cost_per_item + processing_fee) }
⌛️solution3: return custom error message
todo() // return Err(String::from("invalid digit found in string")),
errors3: ? -> return Result or Option to accept ?
// errors3.rs // This is a program that is trying to use a completed version of the // `total_cost` function from the previous exercise. It's not working though! // Why not? What should we do to fix it? // Execute `rustlings hint errors3` or use the `hint` watch subcommand for a hint. // I AM NOT DONE use std::num::ParseIntError; fn main() { let mut tokens = 100; let pretend_user_input = "8"; let cost = total_cost(pretend_user_input)?; if cost > tokens { println!("You can't afford that many!"); } else { tokens -= cost; println!("You now have {} tokens.", tokens); } } pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> { let processing_fee = 1; let cost_per_item = 5; let qty = item_quantity.parse::<i32>()?; Ok(qty * cost_per_item + processing_fee) }
Hint
If other functions can return a
Result, why shouldn’tmain?
It’s a fairly common
convention to return something like Result<(), ErrorType> from your main function.
The unit (()) type is there because nothing is really needed in terms of positive
results.
⚡️️solution: just like async fn to accept await -> return Result<T, E> to chain ?
fn main() -> Result<(), ParseIntError>{ // main function also could return Result<T, E> let mut tokens = 100; let pretend_user_input = "8"; let cost = total_cost(pretend_user_input)?; // should return Result to accept ?, if got error, here will return ParseIntError if cost > tokens { println!("You can't afford that many!"); } else { tokens -= cost; println!("You now have {} tokens.", tokens); } Ok(()) // if successed, here return the OK result () }
errors4
// errors4.rs // Execute `rustlings hint errors4` or use the `hint` watch subcommand for a hint. // I AM NOT DONE #[derive(PartialEq, Debug)] struct PositiveNonzeroInteger(u64); #[derive(PartialEq, Debug)] enum CreationError { Negative, Zero, } impl PositiveNonzeroInteger { fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> { // Hmm...? Why is this only returning an Ok value? Ok(PositiveNonzeroInteger(value as u64)) } } // convert unit tests to main fn main() { fn test_creation() { assert!(PositiveNonzeroInteger::new(10).is_ok()); assert_eq!( Err(CreationError::Negative), PositiveNonzeroInteger::new(-10) ); assert_eq!(Err(CreationError::Zero), PositiveNonzeroInteger::new(0)); } test_creation(); } #[test] fn test_creation() { assert!(PositiveNonzeroInteger::new(10).is_ok()); assert_eq!( Err(CreationError::Negative), PositiveNonzeroInteger::new(-10) ); assert_eq!(Err(CreationError::Zero), PositiveNonzeroInteger::new(0)); }
Hint
PositiveNonzeroInteger::newis always creating a new instance and returning anOkresult.
It should be doing some checking, returning an Err result if those checks fail, and only
returning an Ok result if those checks determine that everything is… okay :)
solution1: use if to catch err
impl PositiveNonzeroInteger { fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> { // Hmm...? Why is this only returning an Ok value? if value < 0 { Err(CreationError::Negative) } else if value == 0 { Err(CreationError::Zero) } else { Ok(PositiveNonzeroInteger(value as u64)) } } }
solution2: use match to catch err
impl PositiveNonzeroInteger { fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> { // Hmm...? Why is this only returning an Ok value? match value { v if v < 0 => return Err(CreationError::Negative), v if v == 0 => return Err(CreationError::Zero), _ => return Ok(PositiveNonzeroInteger(value as u64)) } } }
- This exercise uses some concepts that we won’t get to until later in the course, like
Boxand theFromtrait. - It’s not important to understand them in detail right now, but you can read ahead if you like.
For now, think of the
Box<dyn ...>type as an “I want anything that does ???” type, which, given Rust’s usual standards for runtime safety, should strike you as somewhat lenient!
- In short, this particular use case for boxes is for when you want to own a value and you care only that it is a type which implements a particular trait.
- To do so, The Box is declared as of type
Box<dyn Trait>where Trait is the trait the compiler looks for on any value used in that context. - For this exercise, that context is the potential errors which can be returned in a Result.
What can we use to describe both errors? In other words, is there a trait which both errors implement?
errors5
// errors5.rs // This program uses an altered version of the code from errors4. // This exercise uses some concepts that we won't get to until later in the course, like `Box` and the // `From` trait. It's not important to understand them in detail right now, but you can read ahead if you like. // For now, think of the `Box<dyn ...>` type as an "I want anything that does ???" type, which, given // Rust's usual standards for runtime safety, should strike you as somewhat lenient! // In short, this particular use case for boxes is for when you want to own a value and you care only that it is a // type which implements a particular trait. To do so, The Box is declared as of type Box<dyn Trait> where Trait is the trait // the compiler looks for on any value used in that context. For this exercise, that context is the potential errors // which can be returned in a Result. // What can we use to describe both errors? In other words, is there a trait which both errors implement? // Execute `rustlings hint errors5` or use the `hint` watch subcommand for a hint. // I AM NOT DONE use std::error; use std::fmt; use std::num::ParseIntError; // TODO: update the return type of `main()` to make this compile. fn main() -> Result<(), Box<dyn ???>> { let pretend_user_input = "42"; let x: i64 = pretend_user_input.parse()?; println!("output={:?}", PositiveNonzeroInteger::new(x)?); Ok(()) } // Don't change anything below this line. #[derive(PartialEq, Debug)] struct PositiveNonzeroInteger(u64); #[derive(PartialEq, Debug)] enum CreationError { Negative, Zero, } impl PositiveNonzeroInteger { fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> { match value { x if x < 0 => Err(CreationError::Negative), x if x == 0 => Err(CreationError::Zero), x => Ok(PositiveNonzeroInteger(x as u64)), } } } // This is required so that `CreationError` can implement `error::Error`. impl fmt::Display for CreationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let description = match *self { CreationError::Negative => "number is negative", CreationError::Zero => "number is zero", }; f.write_str(description) } } impl error::Error for CreationError {}
Hint
There are two different possible Result types produced within main(), which are
propagated using ? operators.
How do we declare a return type from
main()that allows both?
- Under the hood, the
?operator callsFrom::fromon the error value to convert it to a boxed trait object, aBox<dyn error::Error>. - This boxed trait object is
polymorphic, and since all errors implement theerror::Errortrait, we can capture lots of different errors in one “Box” object.
Check out this section of the book: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the–operator
Read more about boxing errors: ⭐️Recoverable Errors with Result - The Rust Programming Language
Read more about using the ? operator with boxed errors: Other uses of ? - The Rust Programming Language
Using catch-all error types like Box<dyn error::Error> isn’t recommended
for library code:
where callers might want to make decisions based on the error content, instead of printing it out or propagating it further.
Here, we define a custom error type to make it possible for callers to decide what to do next when our function returns an error.
errors6
// errors6.rs // Using catch-all error types like `Box<dyn error::Error>` isn't recommended // for library code, where callers might want to make decisions based on the // error content, instead of printing it out or propagating it further. Here, // we define a custom error type to make it possible for callers to decide // what to do next when our function returns an error. // Execute `rustlings hint errors6` or use the `hint` watch subcommand for a hint. // I AM NOT DONE use std::num::ParseIntError; // This is a custom error type that we will be using in `parse_pos_nonzero()`. #[derive(PartialEq, Debug)] enum ParsePosNonzeroError { Creation(CreationError), ParseInt(ParseIntError), } impl ParsePosNonzeroError { fn from_creation(err: CreationError) -> ParsePosNonzeroError { ParsePosNonzeroError::Creation(err) } // TODO: add another error conversion function here. // fn from_parseint... } fn parse_pos_nonzero(s: &str) -> Result<PositiveNonzeroInteger, ParsePosNonzeroError> { // TODO: change this to return an appropriate error instead of panicking // when `parse()` returns an error. let x: i64 = s.parse().unwrap(); PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation) } // Don't change anything below this line. #[derive(PartialEq, Debug)] struct PositiveNonzeroInteger(u64); #[derive(PartialEq, Debug)] enum CreationError { Negative, Zero, } impl PositiveNonzeroInteger { fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> { match value { x if x < 0 => Err(CreationError::Negative), x if x == 0 => Err(CreationError::Zero), x => Ok(PositiveNonzeroInteger(x as u64)), } } } // convert unit tests to main fn main() { fn test_parse_error() { // We can't construct a ParseIntError, so we have to pattern match. assert!(matches!( parse_pos_nonzero("not a number"), Err(ParsePosNonzeroError::ParseInt(_)) )); } fn test_negative() { assert_eq!( parse_pos_nonzero("-555"), Err(ParsePosNonzeroError::Creation(CreationError::Negative)) ); } fn test_zero() { assert_eq!( parse_pos_nonzero("0"), Err(ParsePosNonzeroError::Creation(CreationError::Zero)) ); } fn test_positive() { let x = PositiveNonzeroInteger::new(42); assert!(x.is_ok()); assert_eq!(parse_pos_nonzero("42"), Ok(x.unwrap())); } test_parse_error(); test_negative(); test_zero(); test_positive(); } #[cfg(test)] mod test { use super::*; #[test] fn test_parse_error() { // We can't construct a ParseIntError, so we have to pattern match. assert!(matches!( parse_pos_nonzero("not a number"), Err(ParsePosNonzeroError::ParseInt(_)) )); } #[test] fn test_negative() { assert_eq!( parse_pos_nonzero("-555"), Err(ParsePosNonzeroError::Creation(CreationError::Negative)) ); } #[test] fn test_zero() { assert_eq!( parse_pos_nonzero("0"), Err(ParsePosNonzeroError::Creation(CreationError::Zero)) ); } #[test] fn test_positive() { let x = PositiveNonzeroInteger::new(42); assert!(x.is_ok()); assert_eq!(parse_pos_nonzero("42"), Ok(x.unwrap())); } }
Hint
This exercise uses a completed version of
PositiveNonzeroIntegerfrom errors4.
- Below the line that TODO asks you to change, there is an example of using the
map_err()method on aResultto transform one type of error into another. - Try using something similar on the
Resultfromparse(). - You might use the
?operator to return early from the function - or you might use a
matchexpression, or maybe there’s another way!
You can create another function inside
impl ParsePosNonzeroErrorto use withmap_err().
Read more about map_err() in the std::result documentation:
Result in std::result - Rust
solution: define a custom error type
impl ParsePosNonzeroError { fn from_creation(err: CreationError) -> ParsePosNonzeroError { ParsePosNonzeroError::Creation(err) } // TODO: add another error conversion function here. // fn from_parseint... fn from_parseint(err: ParseIntError) -> ParsePosNonzeroError { ParsePosNonzeroError::ParseInt(err) } } fn parse_pos_nonzero(s: &str) -> Result<PositiveNonzeroInteger, ParsePosNonzeroError> { // TODO: change this to return an appropriate error instead of panicking // when `parse()` returns an error. // let x: i64 = s.parse().unwrap(); let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?; PositiveNonzeroInteger::new(x) .map_err(ParsePosNonzeroError::from_creation) }