Error handling

  1. Most errors aren’t serious enough to require the program to stop entirely.
  2. 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.”

  1. 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

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) and Err(String)
  • change the body of the function to return Ok(stuff) where it currently returns Some(stuff)
  • change the body of the function to return Err(error message) where it currently returns None

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))
    }
}
  1. 这里其实单元测试代码已经指出要报错的内容
  2. 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_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!

⭐️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

  1. One way to handle this is using a match statement on item_quantity.parse::<i32>() where the cases are Ok(something) and Err(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!
  2. Take a look at this section of the match expression part 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’t main?

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::new is always creating a new instance and returning an Ok result.

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 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?

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 calls From::from on the error value to convert it to a boxed trait object, a Box<dyn error::Error>.
  • This boxed trait object is polymorphic, and since all errors implement the error::Error trait, 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

solution: error::Error trait

Box<dyn error::Error>

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 PositiveNonzeroInteger from errors4.

  • Below the line that TODO asks you to change, there is an example of using the map_err() method on a Result to transform one type of error into another.
  • Try using something similar on the Result from parse().
  • You might use the ? operator to return early from the function
  • or you might use a match expression, or maybe there’s another way!

You can create another function inside impl ParsePosNonzeroError to use with map_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)
}