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:
- Negative
Err
- Zero
Err
- Positive
Ok
so let's us match
to handle each one of these cases.
- For
Negative
we create a simpleif
statement, saying that if ourv(alue)
is less than0
we return an error - For
Zero
we simply have to match an actual0
- 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
- if our value is less than zero
- if our value is equal to zero
- 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.
- We have to add an error conversion function
- Change the
parse_pos_nonzero
function to allow proper error handling - 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 aResult
to transform one type of error into another. Try using something similar on theResult
fromparse()
. You might use the?
operator to return early from the function, or you might use amatch
expression, or maybe there's another way! You can create another function insideimpl ParsePosNonzeroError
to use withmap_err()
. Read more aboutmap_err()
in thestd::result
documentation: https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err
So let's try this approach.
-
We attempt to parse
s
into ani64
. If this fails, we convert the error and return early because of the?
operator. -
If parsing succeeds, we then attempt to create a
PositiveNonzeroInteger
. If this fails, we convert the error usingmap_err()
, but since there's no?
operator here, we don't return early (because this is the last line of the function and the resultingResult
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.