Rust Error Handling
When a program does several steps that can each fail, checking every single Result with a match becomes long and repetitive. Rust has a small but wonderful tool for this: the question mark operator, ?.
The ? operator
Put ? after something that returns a Result. If it is Ok, you get the value and carry on. If it is Err, the function stops right there and hands the error back to its caller. It is like a colleague who says "if anything goes wrong, I will let the boss know and stop, otherwise I will keep going".
use std::num::ParseIntError;
fn add_texts(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x: i32 = a.trim().parse()?;
let y: i32 = b.trim().parse()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_texts("20", "22"));
println!("{:?}", add_texts("20", "twenty"));
}Ok(42)
Err(ParseIntError { kind: InvalidDigit })Without ?, each parse would need its own match. With it, the function reads like the happy path, and errors still cannot be ignored. You can only use ? inside a function that itself returns a Result (or an Option), because the error needs somewhere to go.
main can return a Result too
If main returns Result<(), String>, you can use ? right inside it. When an error reaches the end of main, Rust prints it and the program ends with a failure code:
fn parse_temperature(text: &str) -> Result<f64, String> {
text.parse::<f64>().map_err(|_| format!("{:?} is not a temperature", text))
}
fn main() -> Result<(), String> {
let morning = parse_temperature("18.5")?;
let afternoon = parse_temperature("27")?;
println!("It warmed up by {} degrees", afternoon - morning);
Ok(())
}It warmed up by 8.5 degrees
map_err turns one kind of error into another, here into a friendly String. Ok(()) at the end means "everything worked, and there is nothing to give back".
Your own error type
For bigger programs, an enum of possible errors is clearer than plain text. Each variant names one thing that can go wrong, and match makes sure every kind is handled:
enum SignupError {
TooShort,
MissingAt,
}
fn check_email(email: &str) -> Result<(), SignupError> {
if email.len() < 6 {
return Err(SignupError::TooShort);
}
if !email.contains('@') {
return Err(SignupError::MissingAt);
}
Ok(())
}
fn main() {
for email in ["a@b", "hello.example.com", "meera@example.com"] {
let verdict = match check_email(email) {
Ok(()) => "looks good",
Err(SignupError::TooShort) => "is too short",
Err(SignupError::MissingAt) => "needs an @",
};
println!("{} {}", email, verdict);
}
}a@b is too short hello.example.com needs an @ meera@example.com looks good
Choosing between panic and Result
A simple rule of thumb: use Result for problems that can reasonably happen, such as a missing file or a typo in the input, so the caller can decide what to do. Stop the program (panic) only for situations that mean there is a bug in the program itself. The next topic looks at panics.
Practice exercises
- Write
fn average(a: &str, b: &str, c: &str) -> Result<f64, ParseIntError>using?three times. - Make
mainreturnResult<(), String>and use?on a value that fails. Read what is printed. - Add a
HasSpacesvariant toSignupErrorand handle it everywhere the compiler asks you to.