Step 1 of 7
panic! or Result?
Programs fail. A user types twelve where you asked for a number, a config file is missing, a customer tries to withdraw more money than they have. Rust splits failures into two kinds and gives each its own tool:
- Unrecoverable errors are bugs: situations the code should never get into, like reading element 10 of a 3-element array. Rust stops the program with a panic.
- Recoverable errors are things that go wrong in normal use: bad input, a missing file, not enough money. A function that can fail this way returns a
Result, and the caller decides what to do about it.
Rust has no exceptions. Whether a function can fail is written in its signature, and the compiler makes sure the caller deals with it.
panic!
You've already met panics: indexing past the end of an array and integer overflow both panic. You can also panic on purpose with the panic! macro, which takes a message like println! does:
fn main() {
println!("loading level...");
let level = 7;
if level > 5 {
panic!("level {level} does not exist");
}
println!("level loaded");
}
loading level...
The program stops at the panic!. The message level 7 does not exist goes to the error output (stderr), along with where the panic happened: panicked at src/main.rs:5:9: (file, line, column). A panic means "the code is wrong": the fix is to change the program, not to try again.
Result
For failures that are part of normal life, a function returns Result<T, E>, an enum from the standard library:
enum Result<T, E> {
Ok(T),
Err(E),
}
T and E stand for types that you choose (module 21 calls them generic type parameters). T is the type of the value on success, E the type of the error. A function returning Result<u32, String> gives back either Ok(some_u32) or Err(some_string). Like Some and None, the variants Ok and Err can be used without writing Result:: in front.
Here's a bank withdrawal that can fail:
fn withdraw(balance: u32, amount: u32) -> Result<u32, String> {
if amount > balance {
return Err(format!("cannot withdraw {amount}: balance is only {balance}"));
}
Ok(balance - amount)
}
fn main() {
println!("{:?}", withdraw(100, 30));
println!("{:?}", withdraw(100, 250));
}
Ok(70)
Err("cannot withdraw 250: balance is only 100")
Piece by piece:
- The return type
Result<u32, String>is part of the function's contract. Anyone calling it can see that it may fail, and that the error is aStringmessage. return Err(...)wraps the error and sends it back early.Ok(balance - amount)wraps the new balance. As the last expression, it's the return value.{:?}prints the wholeResult, so you can see which variant came back.
Think about what a panic would mean here: one customer asking for too much would crash the bank's server for everyone. Returning an Err lets the caller show "insufficient funds" and carry on.
Common mistakes
- Using a
Resultas if it were the value. Parsing can fail, soparsegives aResult, never a plain number:
fn main() {
let n: i32 = "5".parse::<i32>();
println!("{n}");
}
error[E0308]: mismatched types
| let n: i32 = "5".parse::<i32>();
| --- ^^^^^^^^^^^^^^^^^^ expected `i32`, found `Result<i32, ParseIntError>`
The next steps show the ways to get the number out.
- Forgetting the
Ok(...). Endingwithdrawwithbalance - amountgivesexpected Result<u32, String>, found u32. The success value has to be wrapped too. - Ignoring a
Result. Callingwithdraw(100, 250);and throwing the answer away compiles, but rustc warns:unused Result that must be used. Silently ignoring a failure is almost always a bug. - Panicking on bad input.
panic!is for bugs. A typo from a user is not a bug in your program, so it deserves aResult.
Your task
Fill in the blanks so withdraw returns an error when the amount is larger than the balance, and the new balance otherwise. The program should print:
Ok(70)
Err("insufficient funds")
Challenge 2
A sign-up form asks for the user's age. Write fn validate_age(age: i32) -> Result<u32, String>:
- a negative age returns
Err("age cannot be negative") - an age above 150 returns
Err("<age> is not a realistic age"), for exampleErr("200 is not a realistic age") - anything else returns the age as a
u32inOk(useage as u32)
Challenge 3
A command-line tool asks "Continue? (yes/no)". Write fn parse_yes_no(answer: &str) -> Result<bool, String>:
- ignore whitespace around the answer (spaces, a trailing newline) and upper/lower case
yoryesgivesOk(true),nornogivesOk(false)- anything else gives
Err("expected yes or no, got '<answer>'")with the trimmed answer, for exampleErr("expected yes or no, got 'maybe'")