"Handle all errors" is great advice! ...unless you end up with boilerplate like this:
fn is_even(text: &str) -> Result<bool, std::num::ParseIntError> {
let number = match text.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e),
};
Ok(number % 2 == 0)
}
The ? operator is shorthand for that pattern. Put it after a Result
expression to get the value from Ok and continue. If the result is Err, the
function returns the error immediately.
fn is_even(text: &str) -> Result<bool, std::num::ParseIntError> {
let number = text.parse::<i32>()?;
Ok(number % 2 == 0)
}
For these exercises, ? on a Result needs a function that also returns a
Result. It also works on Option inside functions returning Option, where
None causes an early return. To use ? on a Result in main, give main a
Result return type.
? is Rust's shortcut for "if this is Err, return it from the current
function; otherwise, unwrap the value and continue."
Here both calls to parse() return the same error type
(ParseIntError),
so ? works directly without any conversion. Compare to writing this out with a
match on each parse() result.
Useful from the Standard Library
str::parsereturnsResult<T, T::Err>. Combined with?you get the parsed number on the happy path and an early-return on failure.std::num::ParseIntErroris the error type for integer parses. The function signature declares it directly, so?doesn't need to convert anything.
/// Adds two parsed numbers. Compare this to doing it with match statements.
fn add_parsed_numbers(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let a = a.parse::<i32>()?;
let b = b.parse::<i32>()?;
Ok(a + b)
}
#[test]
fn test_add_parsed_numbers() {
assert_eq!(add_parsed_numbers("10", "20"), Ok(30));
assert!(add_parsed_numbers("abc", "10").is_err());
}
Here you use the same operator with a different error type. File I/O returns
std::io::Error, and this
function declares that same error type so ? can pass failures back unchanged.
? doesn't care which concrete error type is involved. It needs the surrounding
function to return the same error type, or one it can convert into with From.
When ? fails to compile, I would check those two error types first. Working
out the conversion can take a few tries; this exercise avoids that by using
std::io::Error throughout.
Useful from the Standard Library
std::fs::read_to_stringreads the whole file into aString. The returnedResult<String, io::Error>matches this function's error type.str::linesiterates over the file's lines without keeping the trailing newlines.Iterator::countconsumes the iterator and returns how many lines there were.- Once you have the file contents,
lines()andcount()can remain in the same expression.
This test creates test.txt in the current working directory and removes it
afterward. It also expects missing.txt not to exist. Avoid running multiple
copies of this test in the same directory: they can overwrite or remove each
other's file. For tests in one process that share a path,
cargo test -- --test-threads=1 prevents them from running at the same time.
Separate test processes still need separate paths.
In production tests,
tempfile::NamedTempFile
creates a uniquely named file and removes it when dropped.
/// Reads a file and counts lines. Note how `?` works with a different error
/// type.
fn count_file_lines(filename: &str) -> Result<usize, std::io::Error> {
let content = std::fs::read_to_string(filename)?;
Ok(content.lines().count())
}
#[test]
fn test_count_file_lines() {
use std::fs;
fs::write("test.txt", "line 1\nline 2").unwrap();
assert_eq!(count_file_lines("test.txt").unwrap(), 2);
assert!(count_file_lines("missing.txt").is_err());
fs::remove_file("test.txt").ok();
}
add_parsed_numbers had two chances to return a parse error. sum_numbers may
inspect many tokens, but it still returns only the first parse error it
encounters.
sum_numbers takes text with integers separated by whitespace and adds them up.
The first token that isn't a number makes the function return that
ParseIntError and stop. Since the function only parses, one error type covers
every failure without boxing or conversion.
Use a for loop and apply ? to each token's parse result. What happens to the
rest of the loop when parsing fails? Return Ok(0) for empty or whitespace-only
input. Assume the running total fits in an i32.
Useful from the Standard Library
str::split_whitespaceyields each token as a&str, skipping the gaps between numbers.str::parsereturns aResult;parse::<i32>()asks for an integer.
/// Sums a whitespace-separated list of integers held in `text`.
///
/// Each token is parsed with `?`: the first one that isn't a number
/// short-circuits and returns its `ParseIntError`. The function only parses (no
/// file I/O), so a single error type is enough and there's no need for
/// `Box<dyn Error>`.
fn sum_numbers(text: &str) -> Result<i32, std::num::ParseIntError> {
let mut total = 0;
for token in text.split_whitespace() {
total += token.parse::<i32>()?;
}
Ok(total)
}
#[test]
fn test_sum_numbers() {
assert_eq!(sum_numbers("5\n10\n15").unwrap(), 30);
assert_eq!(sum_numbers(" 1 2 3 ").unwrap(), 6);
assert!(sum_numbers("5\nabc\n15").is_err()); // not a number
}
#[test]
fn test_sum_numbers_empty_and_signed() {
assert_eq!(sum_numbers(""), Ok(0));
assert_eq!(sum_numbers(" \t\n"), Ok(0));
assert_eq!(sum_numbers("-5 +2 3"), Ok(0));
}
#[test]
fn test_sum_numbers_first_error() {
let first_error = "abc".parse::<i32>().unwrap_err();
assert_eq!(sum_numbers("5 abc 99999999999999999999"), Err(first_error));
}
You replaced repetitive match chains with ?, propagated errors out of
multi-step functions, and used ? inside a loop.
What We Learned
?is shorthand for "if this isErr, return it from the current function; if it'sOk, unwrap the value and keep going." It works onOptiontoo (returningNoneearly).- For
Result, the surrounding function must return aResultwith the same error type, or one the error converts into viaFrom. ForOption, the surrounding function returnsOption; there is no error value to convert.- Inside a loop, a failed
?returns from the whole function, not just the current iteration. Later tokens are never parsed.- Every exercise here used a single error type, so
?propagated with no conversion. When a function mixes error types (say file I/O and parsing), you need a common error type. The env-file parser usesBox<dyn Error>as a common error type.- Tests that share a filesystem path can race when they run in parallel. Use separate paths;
--test-threads=1only serializes tests within one test process.
Extra practice to explore at your own pace. These chapters do not count toward course progress.