Error Handling
The
Resultenum is used to indicate success or failure.enum Result<T, E> { Ok(T), Err(E), }This is very useful especially in production code, I think we will use more custom error enum.
The
?operator simplifies error propagation.If the result is
Err, it propagates the error.If
Ok, it unwraps the value.
When your function can produce multiple error types, we can use
Box<dyn std::error::Error>to erase the concrete error type.This simplifies the return type while still maintaining flexibility.
References:
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 returning `None`. Thankfully, Rust has a similar
// construct to `Option` that can be used to express error conditions. Change
// the function signature and body to return `Result<String, String>` instead
// of `Option<String>`.
fn generate_nametag_text(name: String) -> Result<String, String> {
if name.is_empty() {
// Empty names aren't allowed
Err("Empty names aren't allowed".to_string())
} else {
Ok(format!("Hi! My name is {name}"))
}
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generates_nametag_text_for_a_nonempty_name() {
assert_eq!(
generate_nametag_text("Beyoncé".to_string()).as_deref(),
Ok("Hi! My name is Beyoncé"),
);
}
#[test]
fn explains_why_generating_nametag_text_fails() {
assert_eq!(
generate_nametag_text(String::new())
.as_ref()
.map_err(|e| e.as_str()),
Err("Empty names aren't allowed"),
);
}
}In this exercise we need to fix the return type in function
generate_nametag_textto useResult<String, String>enum.The
Resultenum is used to indicate success or failure.Use
Okif success.Use
Errif not.
Then if given
nameis empty we returnErr("Empty names aren't allowed".to_string())instead ofNone.If not empty then we should return
Ok(format!("Hi! My name is {name}")).
errors2.rs
use std::num::ParseIntError;
fn total_cost(item_quantity: &str) -> Result<i32, ParseIntError> {
let processing_fee = 1;
let cost_per_item = 5;
// Add ? to propagate the error.
let qty = item_quantity.parse::<i32>()?;
Ok(qty * cost_per_item + processing_fee)
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
use std::num::IntErrorKind;
#[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().kind(),
&IntErrorKind::InvalidDigit,
);
}
}In this exercise we just need to propagate the error.
We can use the
?operator.If the result is
Err, it propagates the error.If Ok, it unwraps the value.
So it will be like this:
let qty = item_quantity.parse::<i32>()?;
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?
use std::num::ParseIntError;
// Don't change this function.
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)
}
// Add return here
fn main() -> Result<(), ParseIntError> {
let mut tokens = 100;
let pretend_user_input = "8";
// Don't change this line.
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.");
}
Ok(()) // call Ok
}In this exercise we need to fix the error propagation.
Inside the
mainfunction as we can se it also propagate the error from calling functiontotal_cost.But the
mainfunction doesn't return anything so it got compile error.To fix this we can add
Result<(), ParseIntError>as return type inmainfunction.We also need to add
Ok(())at the end to mark as the main function is done without any error.Reference: https://stackoverflow.com/a/50459909/903350
errors4.rs
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
// This function shouldn't always return an `Ok`.
match value.cmp(&0) {
std::cmp::Ordering::Less => Err(CreationError::Negative),
std::cmp::Ordering::Equal => Err(CreationError::Zero),
std::cmp::Ordering::Greater => Ok(Self(value as u64)),
}
}
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_creation() {
assert_eq!(
PositiveNonzeroInteger::new(10),
Ok(PositiveNonzeroInteger(10)),
);
assert_eq!(
PositiveNonzeroInteger::new(-10),
Err(CreationError::Negative),
);
assert_eq!(PositiveNonzeroInteger::new(0), Err(CreationError::Zero));
}
}In this exercise we need complete the function
PositiveNonzeroInteger::new.If the value is
< 0returnCreationError::Negative.If the value is
== 0returnCreationError::Negative.Else return
Self(value as u64).
We can do this in multiple ways.
We can use classic
ifsyntax like this:if value < 0 { Err(CreationError::Negative) } else if value == 0 { Err(CreationError::Zero) } else { Ok(Self(value as u64)) }Or we can use match syntax like this:
match value.cmp(&0) { std::cmp::Ordering::Less => Err(CreationError::Negative), std::cmp::Ordering::Equal => Err(CreationError::Zero), std::cmp::Ordering::Greater => Ok(Self(value as u64)), }Or we also can combine both
matchandif.All of them should fix the code.
errors5.rs
use std::error::Error;
use std::fmt;
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}
// This is required so that `CreationError` can implement `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 for CreationError {}
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<PositiveNonzeroInteger, CreationError> {
match value {
x if x < 0 => Err(CreationError::Negative),
0 => Err(CreationError::Zero),
x => Ok(PositiveNonzeroInteger(x as u64)),
}
}
}
// Add the correct return type `Result<(), Box<dyn ???>>`. What can we
// use to describe both errors? Is there a trait which both errors implement?
fn main() -> Result<(), Box<dyn Error>> {
let pretend_user_input = "42";
let x: i64 = pretend_user_input.parse()?;
println!("output={:?}", PositiveNonzeroInteger::new(x)?);
Ok(())
}Similar like exercise
errors3.rswe want to propagate the error out of themainfunction.But in this case we have multiple kind/variant of
Error.To make the returned error dynamic we can use
Box<dyn Error>like this:fn main() -> Result<(), Box<dyn Error>> { }
errors6.rs
// Using catch-all error types like `Box<dyn 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.
use std::num::ParseIntError;
#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}
// A custom error type that we will be using in `PositiveNonzeroInteger::parse`.
#[derive(PartialEq, Debug)]
enum ParsePosNonzeroError {
Creation(CreationError),
ParseInt(ParseIntError),
}
impl ParsePosNonzeroError {
fn from_creation(err: CreationError) -> Self {
Self::Creation(err)
}
// Add another error conversion function here.
fn from_parse_int(err: ParseIntError) -> Self {
Self::ParseInt(err)
}
}
#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);
impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
match value {
x if x < 0 => Err(CreationError::Negative),
0 => Err(CreationError::Zero),
x => Ok(Self(x as u64)),
}
}
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
// change this to return an appropriate error instead of panicking
// when `parse()` returns an error.
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?;
Self::new(x).map_err(ParsePosNonzeroError::from_creation)
}
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_error() {
assert!(matches!(
PositiveNonzeroInteger::parse("not a number"),
Err(ParsePosNonzeroError::ParseInt(_)),
));
}
#[test]
fn test_negative() {
assert_eq!(
PositiveNonzeroInteger::parse("-555"),
Err(ParsePosNonzeroError::Creation(CreationError::Negative)),
);
}
#[test]
fn test_zero() {
assert_eq!(
PositiveNonzeroInteger::parse("0"),
Err(ParsePosNonzeroError::Creation(CreationError::Zero)),
);
}
#[test]
fn test_positive() {
let x = PositiveNonzeroInteger::new(42).unwrap();
assert_eq!(x.0, 42);
assert_eq!(PositiveNonzeroInteger::parse("42"), Ok(x));
}
}In this exercise we need to finish the custom error.
We need to add
from_parse_intfunction like this:fn from_parse_int(err: ParseIntError) -> Self { Self::ParseInt(err) }The we need to change the parse code.
So instead of unwrap we want to map the error using
map_errormethod and propagate the error like this:let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?;
Last updated