15 Rustlings Errors Part 2 Solution

Error Handling Part 2

We're back with the 2nd part of our Error handling post we'll cover exercises 3-6. If you haven't read Error Handling Part 1 it's a good idea to go through that first before moving on to these.

This is from the Rustlings README:

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.

Further information

Errors4.rs

// 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))
    }
}

#[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));
}

We don't get much for hints and description, other than a comment that asks Hmm...? Why is this only reguring an Ok value?

Let's look at our Rust compiler errors and see if we get any additional hints on how to fix this.

Errors4.rs errors

⚠️  Testing of exercises/error_handling/errors4.rs failed! Please try again. Here's the output:

running 1 test
test test_creation ... FAILED

successes:

successes:

failures:

---- test_creation stdout ----
thread 'test_creation' panicked at 'assertion failed: `(left == right)`
  left: `Err(Negative)`,
 right: `Ok(PositiveNonzeroInteger(18446744073709551606))`', exercises/error_handling/errors4.rs:25:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    test_creation

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

It looks like our code is compiling but panicking and we see that our test is failing. If we look at the actual test we see that (left == right) failed we're getting different values. The test is expecting Err(Negative) but instead it's seeing Ok(PositiveNonzero...))

Errors4.rs solution

It looks like we have to handle our error's, as nothing is being done to take care of them in our code right now. Maybe an if or maybe a match statement will do the trick. Let's try. Let's use a match statement first. If we look at the code and tests, we know that there is 3 different scenarios we have to account for:

  1. Negative Err
  2. Zero Err
  3. Positive Ok

so let's us match to handle each one of these cases.

  1. For Negative we create a simple if statement, saying that if our v(alue) is less than 0 we return an error
  2. For Zero we simply have to match an actual 0
  3. For positive, it can be anything positive meaning we can use the _ wildcard underscore character
match value {
	v if v < 0 => Err(CreationError::Negative), // v represents `value` here
	0 => Err(CreationError::Zero),
	_ => Ok(PositiveNonzeroInteger(value as u64)),
}

It would look like this in the context of our code.

#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);

#[derive(PartialEq, Debug)]
enum CreationError {
    Negative,
    Zero,
}

impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
        match value {
            v if v < 0 => Err(CreationError::Negative),
            0 => Err(CreationError::Zero),
            _ => Ok(PositiveNonzeroInteger(value as u64)),
        }
    }
}

#[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));
}

And this compiles 🎉 but can we use if in this as well?

What about if

Let's take a stab at it. So, again we have to deal with the 3 different scenarios here

  1. if our value is less than zero
  2. if our value is equal to zero
  3. if it's positive

So writing that looks like this:

 if value < 0 {
            Err(CreationError::Negative)
        } else if value == 0 {
            Err(CreationError::Zero)
        } else {
            Ok(PositiveNonzeroInteger(value as u64))
        }

Here's what it looks like in context of the full problem:

#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);

#[derive(PartialEq, Debug)]
enum CreationError {
    Negative,
    Zero,
}

impl PositiveNonzeroInteger {
    fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
        if value < 0 {
            Err(CreationError::Negative)
        } else if value == 0 {
            Err(CreationError::Zero)
        } else {
            Ok(PositiveNonzeroInteger(value as u64))
        }
    }
}

#[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));
}

and with that we have solved errors4.rs we have our code compiling:

✅ Successfully tested exercises/error_handling/errors4.rs!

🎉 🎉  The code is compiling, and the tests pass! 🎉 🎉

Alright. On to Errors5.rs

Errors5.rs

// 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 {}

This is an interesting one as it has a lot of comments above the code but it mainly provides hints about what Box can do. Our task is to determine the correct return type in the Rusult<(), Box<dyn ???>> part of the code. The easy thought is that it has to return an Error but is it that simple? Let's try, but first let's take a look at the errors.

Errors5.rs errors

⚠️  Compiling of exercises/error_handling/errors5.rs failed! Please try again. Here's the output:
error: expected identifier, found `>>`
  --> exercises/error_handling/errors5.rs:26:36
   |
26 | fn main() -> Result<(), Box<dyn ???>> {
   |                                    ^^ expected identifier

error: aborting due to previous error

Nothing to specific here but we see where we are missing our code and it's expecting an identifier.

Errors5.rs solution

So let's try simply entering Error because that's what we should be expecting to see correct?

// 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(())
}

Well, we still get some errors but we have some help.

⚠️  Compiling of exercises/error_handling/errors5.rs failed! Please try again. Here's the output:
error[E0405]: cannot find trait `Error` in this scope
  --> exercises/error_handling/errors5.rs:26:33
   |
26 | fn main() -> Result<(), Box<dyn Error>> {
   |                                 ^^^^^ not found in this scope
   |
help: consider importing one of these items
   |
21 + use core::error::Error;
   |
21 + use crate::error::Error;
   |
21 + use std::error::Error;

So the compiler is telling us that that Error is not found in scope and gives us a bunch of different options to use in our Box<dyn >. But before we implement one of these let's continue looking at our code. I had missed this comment earlier:// This is required so that `CreationError` can implement `error::Error`. It looks like we might have a hint here too. This comment is telling us that this function is being created to allow us to use error::Error for CreationError which means that we could use error::Error as our error handler in the return type right? Let's try.

// TODO: update the return type of `main()` to make this compile.
fn main() -> Result<(), Box<dyn error::Error>> {
    let pretend_user_input = "42";
    let x: i64 = pretend_user_input.parse()?;
    println!("output={:?}", PositiveNonzeroInteger::new(x)?);
    Ok(())
}

It compiles! This is our output:

✅ Successfully ran exercises/error_handling/errors5.rs!

🎉 🎉  The code is compiling! 🎉 🎉

Output:
====================
output=PositiveNonzeroInteger(42)

====================

Errors6.rs

// 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)),
        }
    }
}

#[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()));
    }
}

In this exercise we are defining a custom error type and we have 3 clear instructions.

  1. We have to add an error conversion function
  2. Change the parse_pos_nonzero function to allow proper error handling
  3. Don't change anything below code line 38 or after the parse_pos_nonzero function ends.

Errors6.rs errors

⚠️  Testing of exercises/error_handling/errors6.rs failed! Please try again. Here's the output:

running 4 tests
test test::test_negative ... ok
test test::test_positive ... ok
test test::test_zero ... ok
test test::test_parse_error ... FAILED

successes:

successes:
    test::test_negative
    test::test_positive
    test::test_zero

failures:

---- test::test_parse_error stdout ----
thread 'test::test_parse_error' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', exercises/error_handling/errors6.rs:33:28
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    test::test_parse_error

test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Our errors show us we have 3 passing test but the test_parse_error test is failing.

Errors6.rs solution

Let's first start by creating that 2nd function.


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)
    }
    fn from_parseint(err: ParseIntError) -> ParsePosNonzeroError {
        ParsePosNonzeroError::ParseInt(err)
    }

}

Here i'm creating a new function using the same structure as the from_creation and this should help us finish our custom error types. When we save though, nothing changes, we still get 3 passing tests, and one fail.

So let's move on to the next todo on our list.

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)
}

Parse Error Approach

So we know that we have to remove the .unwrap() method because this is what makes the code panic, and if we look at the hints we get this:

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: https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err

So let's try this approach.

  1. We attempt to parse s into an i64. If this fails, we convert the error and return early because of the ? operator.

  2. If parsing succeeds, we then attempt to create a PositiveNonzeroInteger. If this fails, we convert the error using map_err(), but since there's no ? operator here, we don't return early (because this is the last line of the function and the resulting Result is directly returned).

fn parse_pos_nonzero(s: &str) -> Result<PositiveNonzeroInteger, ParsePosNonzeroError> {
    // Try parsing the string into an i64.
    let x = s.parse::<i64>().map_err(ParsePosNonzeroError::from_parseint)?;

    // Create a PositiveNonzeroInteger, and if this fails, transform the error.
    PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation)
}

Using Match

We can also use matching to get our result, using something called a turbofish (unofficially) the full scoop on this this should be later, but for now let's say this ::<> or turbofish (see it kinda looks like a fish) is used to help the compiler understand the exact type you intend when type inference might be insufficient or ambiguous.

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 = s.parse::<i64>();

    match x {
        Ok(val) => PositiveNonzeroInteger::new(val).map_err(ParsePosNonzeroError::from_creation),
        Err(e) => Err(ParsePosNonzeroError::from_parseint(e)),
    }
}

Conclusion

In this second part of our exploration into Rust error handling, we delved into exercises 3 to 6 to understand and tackle various aspects of working with errors in Rust programming. We built upon the concepts introduced in the first part and extended our knowledge to handle more intricate scenarios.

We began with Errors4.rs, where we encountered a situation involving custom error types. By using the match expression, we effectively handled different error cases and returned appropriate results based on the input. This exercise illuminated the power of pattern matching in Rust's error handling mechanism.

Moving on to Errors5.rs, we tackled a case involving the Box<dyn Error> type. By correctly choosing the appropriate return type for the main() function, we demonstrated how Rust's error system allows for flexibility in dealing with various error types in a generic manner. This exercise reinforced the importance of choosing the right error type depending on the context of your program.

Finally, in Errors6.rs, we showcased our ability to create custom error types and handle complex error propagation scenarios. We introduced a custom error type ParsePosNonzeroError and skillfully converted different error types into this unified form using methods like map_err(). This exercise highlighted Rust's expressive error handling capabilities and provided insight into creating ergonomic APIs that clearly communicate errors to the caller.

Throughout this journey, we harnessed Rust's powerful error handling constructs, including Result, match expressions, ? operator, map_err(), and custom error types. These exercises reinforced the idea that errors in Rust are not just exceptions but a fundamental part of the language that guides developers in creating robust and reliable software.

By mastering error handling in Rust, we equip ourselves with the tools to write code that elegantly handles failures and provides meaningful feedback to users. As we continue our Rust learning journey, these skills will prove invaluable in building resilient and dependable applications.

Continue to explore the rich Rust ecosystem, experiment with error handling in different scenarios, and always remember that Rust's error handling philosophy encourages us to embrace errors, not fear them.