15 Rustlings Errors Part 1 Solution
Error Handling Part 1
There's 6 exercises so I'm breaking them up into batches of 3.
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
errors1.rs
// 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))
}
}
#[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())
);
}
}
Our instructions are to convert our Option
to a Result
, where we can change the the outcome, or rather specify an error as we like vs just returning None
with an Option
.
errors1.rs errors
⚠️ Compiling of exercises/error_handling/errors1.rs failed! Please try again. Here's the output:
error[E0308]: mismatched types
--> exercises/error_handling/errors1.rs:25:9
|
25 | / assert_eq!(
26 | | generate_nametag_text("Beyoncé".into()),
27 | | Ok("Hi! My name is Beyoncé".into())
28 | | );
| | ^
| | |
| |_________expected `Option<String>`, found `Result<_, _>`
| expected because this is `Option<String>`
|
= note: expected enum `Option<String>`
found enum `Result<_, _>`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0308]: mismatched types
--> exercises/error_handling/errors1.rs:33:9
|
33 | / assert_eq!(
34 | | generate_nametag_text("".into()),
35 | | // Don't change this line
36 | | Err("`name` was empty; it must be nonempty.".into())
37 | | );
| | ^
| | |
| |_________expected `Option<String>`, found `Result<_, _>`
| expected because this is `Option<String>`
|
= note: expected enum `Option<String>`
found enum `Result<_, _>`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to 2 previous errors
Our errors here are showing us that we have a mismatch with our types, since the tests are already expecting a Result
but not the Option
that is currently being used. So, let's try and fix that, it doesn't seem too difficult.
errors1.rs solution
Let's start by changing the return type, we know that it has to be a result, so let's do that in the generate_nametag_text
signature.
pub fn generate_nametag_text(name: String) -> Result<String> {
The next thing we have to do is update the return value, because it's going from returning one string to two, so instead of having one String in our Result
we need two Result<String, String>
, and because we've changed the type of from Option
to Result
we also have to update our Some
and None
to what a Result
should have which is Ok
and Err
Our code now looks like this. Let's save our file and see what our compiler says.
pub fn generate_nametag_text(name: String) -> Result<String, String> {
if name.is_empty() {
// Empty names aren't allowed.
Err
} else {
Ok(format!("Hi! My name is {}", name))
}
}
Close but not quite, we see that we still have an error and it's telling us that after our Err
it's expecting a value.
⚠️ Compiling of exercises/error_handling/errors1.rs failed! Please try again. Here's the output:
error[E0308]: mismatched types
--> exercises/error_handling/errors1.rs:13:9
|
10 | pub fn generate_nametag_text(name: String) -> Result<String, String> {
| ---------------------- expected `Result<String, String>` because of return type
...
13 | Err
| ^^^ expected `Result<String, String>`, found enum constructor
|
= note: expected enum `Result<String, String>`
found enum constructor `fn(_) -> Result<_, _> {Result::<_, _>::Err}`
help: use parentheses to construct this tuple variant
|
13 | Err(/* value */)
| +++++++++++++
error: aborting due to previous error
The compiler even shows us where to put the value:
13 | Err(/* value */)
| +++++++++++++
But what value should it be? 🤔 Well in our case we can look at the test value and see what we are expected to write in there.
#[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())
);
}
Here we can clearly see what the text should be that's associated with the Err
, so let's add that to our code.
We use the same format!
macro and it should look like this now:
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))
}
}
and with that our code is compiling and our tests are passing:
🎉 🎉 The code is compiling, and the tests pass! 🎉 🎉
yay us.
Errors2.rs
// 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)
}
#[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"
);
}
}
Our instructions tell us that the total_cost
function is not handling the error case but also not handling the success case properly either. So what we want to is:
- if we call the
parse
function on a string that is not a number it will return an error and that case we want to immediately return that error from our function and not attempt to multiply and add.
We get the hint that we can do this correctly in a couple of ways and one of them is much shorter.
Errors2.rs errors
⚠️ Compiling of exercises/error_handling/errors2.rs failed! Please try again. Here's the output:
error[E0369]: cannot multiply `Result<i32, ParseIntError>` by `{integer}`
--> exercises/error_handling/errors2.rs:29:12
|
29 | Ok(qty * cost_per_item + processing_fee)
| --- ^ ------------- {integer}
| |
| Result<i32, ParseIntError>
error: aborting due to previous error
For more information about this error, try `rustc --explain E0369`.
Errors tell us that we cannot multiply Results<i32, ParseIntError>
by {integer}
. Alright let's try to work on our solution.
Erros2.rs solution
My gut tells me to use matching to set up the Ok
and Err
, let's try:
pub fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
let processing_fee = 1;
let cost_per_item = 5;
// adding `match` statement
let qty = match item_quantity.parse::<i32>() {
Ok(qty) => qty, // matching `Ok`
Err(e) => return Err(e), // matching `Err`
};
Ok(qty * cost_per_item + processing_fee)
}
So, we added match
right before item_quanity.parse::<i32>()
and then filled out the two match arms with Ok
returning qty
as an int
and if it's not an int
then we match the Err
arm and quit out of the process all together by using return Err(e)
.
But if we remember we also were given the idea that we could do this in two ways and although this wasn't particularly long, is there a shorter way?
Errors2.rs solution 2
Yes, there is! If we place a ?
operator after a Result
value it functions in a similar way as a match
statement, there is a difference but for the purposes it does the job just fine, so yea it's a much much shorter way to handle this type of where a couple of lines of code are reduced to one character ?
.
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)
}
Errors3.rs
// 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)
}
In this exercise we are presented with fn main()
which is trying to us our fn total_cost
that we worked on in the previous exercise but there's a problem and we have to figure out. So as always let's look at our errors and see what we can learn there.
Errors3.rs errors
⚠️ Compiling of exercises/error_handling/errors3.rs failed! Please try again. Here's the output:
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> exercises/error_handling/errors3.rs:15:46
|
11 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
...
15 | let cost = total_cost(pretend_user_input)?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, ParseIntError>>` is not implemented for `()`
error: aborting due to previous error
Our error seems quite obvious and the compiler does a great job of pointing it out. We're trying to us the ?
operator, but we have no Rusult
or Option
to use it on since our fn main()
does not define one in it's signature, so let's do that.
Errors3.rs solution
// updating `main` signature to return a result
fn main() -> Result<(), ParseIntError> {
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);
}
}
So if you've read through the Rust by Example provided in the Rustlings README (and above) you would see that we can return the Result
type in main
but it must be the unit
()
type.
Using Result<(), ErrorType>
as the return type for the main
function is a common pattern in Rust for error handling. It allows you to handle errors explicitly and propagate them up the call stack if necessary.
If the main
function returns an Err
value, it will be treated as an error by the Rust runtime, and the program will exit with a non-zero exit code.
Alright our code has a proper function signature let's try and run this...Uh oh. Still not compiling we know get these erros.
⚠️ Compiling of exercises/error_handling/errors3.rs failed! Please try again. Here's the output:
error[E0308]: mismatched types
--> exercises/error_handling/errors3.rs:17:22
|
17 | if cost > tokens {
| ______________________^
18 | | println!("You can't afford that many!");
19 | | } else {
| |_____^ expected `Result<(), ParseIntError>`, found `()`
|
= note: expected enum `Result<(), ParseIntError>`
found unit type `()`
error[E0308]: mismatched types
--> exercises/error_handling/errors3.rs:19:12
|
19 | } else {
| ____________^
20 | | tokens -= cost;
21 | | println!("You now have {} tokens.", tokens);
22 | | }
| |_____^ expected `Result<(), ParseIntError>`, found `()`
|
= note: expected enum `Result<(), ParseIntError>`
found unit type `()`
error: aborting due to 2 previous errors
This error is telling us that there are type mismatches in the main
function of the errors3.rs
file. Specifically, the if
and else
blocks have different return types: the if
block should return Result<(), ParseIntError>
, but it's returning ()
, which is a unit type. The compiler is expecting both branches to return a Result
with the same error type, but it found a unit type instead.
So how do we fix this? To follow the rules of the Result
type in the main
function, we use return Ok(())
in both the if
and else
blocks to indicate success, even though the if
block deals with a case where the user can't afford the items. This way, the compiler knows that we are handling the error properly and can ensure the code is correct.
Our main()
should now look like this:
fn main() -> Result<(), ParseIntError> {
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!");
return Ok(()); // adding the Ok(()) return value
} else {
tokens -= cost;
println!("You now have {} tokens.", tokens);
return Ok(()); // adding the Ok(()) return value
}
}
In the given block of code, there is no need to contain an Err
section because the total_cost
function is already responsible for handling errors related to parsing the input string into an integer. The total_cost
function has the return type Result<i32, ParseIntError>
, and it uses the ?
operator to propagate the error when parsing fails.
The ?
operator in the total_cost
function allows the error to be returned immediately from the function if there is a parsing error. The calling code in the main
function does not need to handle the Err
case again because it is already being handled in the total_cost
function.
To put it in simpler terms, the total_cost
function is like a "guard" that ensures that the parsing is successful. If there is an error, it will return the error to the calling code (the main
function in this case). Since the total_cost
function already handles the error, the main
function doesn't need to worry about it. Instead, it only needs to deal with the case where parsing is successful (Ok
).
So, in this particular case, we don't need an Err
section in the main
function because the error handling is taken care of by the total_cost
function. The main
function only needs to handle the successful case (Ok
), and if the parsing is successful, it will proceed with the code inside the else
block. If parsing fails, the error will be propagated and handled by Rust automatically.
When we save we get this output:
Output:
====================
You now have 59 tokens.
====================
Conclusion of Part 1
In this first part of the Rustlings error handling exercises, we learned about using Result
and Option
types to handle errors and cases where there might not be a valid value. We encountered different scenarios and learned how to convert functions returning Option
into ones returning Result
to provide more specific error messages. Additionally, we saw how to use the ?
operator to propagate errors up the call stack automatically, simplifying the error handling process.
It's essential to handle errors properly in Rust, as it ensures that your program remains robust and can gracefully recover from unexpected situations. Using Result
and Option
types, along with pattern matching and the ?
operator, we can create code that is not only correct but also more readable and maintainable. Rust's strong type system helps catch errors at compile time and guides us in writing safer and more reliable software.
In the next part of the error handling exercises, we will delve deeper into handling different types of errors, exploring how to use Result
with custom error types and how to combine and chain errors effectively. Keep learning and practicing, and happy coding!