12.2Result and recoverable errors
Most failures aren't bugs. A file might not exist; a user might mistype; a number might be too big to parse. The program should handle these and carry on, not panic. Rust's tool for "this operation might fail, and the caller should deal with it" is a type you've been using since chapter 1 without being introduced: Result. And like Option before it, the reveal is that it's just an enum, so everything from chapter 11 already applies.
Result is an enum
Result<T, E> has exactly two variants:
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok(T) carries the success value of type T; Err(E) carries an error value of type E. The two type placeholders mean "on success I give you a T, on failure I give you an E," and a function declares both in its return type. Like Option, Result is in scope everywhere, so you write Ok(...) and Err(...) directly.
You've been calling functions that return Result all along. "42".parse::<i32>() returns Result<i32, ParseIntError>: Ok(42) when the text is a valid integer, Err(...) when it isn't. read_line returns a Result too. The .expect("...") you've tacked onto them since lesson 1.6 was dealing with that Result; now you'll learn what it was really doing.
Matching on a Result
Since Result is an enum, you handle it with match (lesson 11.2), giving the success and failure cases each their own arm:
fn main() {
let text = "42";
match text.parse::<i32>() {
Ok(number) => println!("parsed: {}", number * 2),
Err(error) => println!("not a number: {error}"),
}
}parsed: 84
The Ok(number) arm binds the parsed i32 and uses it; the Err(error) arm binds the error and reports it. Change "42" to "abc" and the same code takes the other branch:
let text = "abc";
match text.parse::<i32>() {
Ok(number) => println!("parsed: {}", number * 2),
Err(error) => println!("not a number: {error}"),
}not a number: invalid digit found in string
No panic, no crash. The program inspected the Result, found an Err, and responded. That's recoverable error handling: the failure is a value you examine and branch on, not an explosion. And exhaustiveness (lesson 11.1) guarantees you can't forget the Err case, which is the entire difference from languages where an ignored error silently propagates.
Key insight
Result makes failure a value, not an interruption. Where other languages throw exceptions that travel invisibly up the stack and may or may not be caught, Rust hands the error back as an ordinary return value of an ordinary enum. You can see in a function's type whether it can fail, and the compiler makes you handle the failure before you can use the success. Errors stop being a parallel control-flow system and become regular data.
unwrap and expect on Result
Writing a full match for every fallible call would be heavy, so Result has the same shortcuts as Option (lesson 11.3), and now you can see precisely what they do. unwrap() returns the Ok value or panics with the error inside; expect("msg") does the same with your message:
fn main() {
let good: i32 = "42".parse().unwrap(); // 42
println!("{good}");
let bad: i32 = "abc".parse().unwrap(); // panics
println!("{bad}");
}42
thread 'main' panicked at src/main.rs:5:30:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
There it is. unwrap on an Err panics (lesson 12.1), printing the contained error. So every .expect("failed to read line") you wrote after read_line meant "if reading fails, panic with this message." It was a deliberate choice to treat a recoverable error as unrecoverable, reasonable in a tiny program where there's nothing better to do, but a choice all the same.
A safer cousin, unwrap_or(default), returns the Ok value or a fallback without panicking, mirroring Option:
fn main() {
let n: i32 = "abc".parse().unwrap_or(0);
println!("{n}"); // 0
}0Best practice
unwrap and expect are fine in examples, prototypes, and tests, and acceptable when an Err truly cannot happen and you can say why. In real programs that should keep running, handle the Err deliberately: with match, with unwrap_or and friends, or by passing the error up to a caller who knows what to do. The next lesson's ? operator makes that last option nearly free.
Returning a Result of your own
You don't only consume Results; you write functions that produce them. A function that can fail says so in its return type, returning Ok on success and Err on failure:
fn parse_percentage(text: &str) -> Result<u8, String> {
match text.parse::<u8>() {
Ok(n) if n <= 100 => Ok(n),
Ok(n) => Err(format!("{n} is over 100")),
Err(_) => Err(format!("'{text}' is not a number")),
}
}
fn main() {
println!("{:?}", parse_percentage("50")); // Ok(50)
println!("{:?}", parse_percentage("150")); // Err("150 is over 100")
println!("{:?}", parse_percentage("xyz")); // Err("'xyz' is not a number")
}Ok(50)
Err("150 is over 100")
Err("'xyz' is not a number")
parse_percentage returns Result<u8, String>: a u8 on success, an error message on failure. It validates two ways a percentage can be wrong (not a number, or out of range) and returns a descriptive Err for each, using a guard (lesson 11.4) to separate the in-range and out-of-range Ok cases. The caller gets a Result they must handle, and the function never panics over bad input, it reports it. Using String as the error type is the simplest choice; lesson 12.6 shows a sturdier one.
Quiz time
Question #1
What are the two variants of Result<T, E>, and what does each carry?
Show solution
Ok(T) carries the success value of type T; Err(E) carries the error value of type E. A function returning Result<T, E> gives back Ok with a T when it succeeds and Err with an E when it fails.
Question #2
What does "abc".parse::<i32>().unwrap() do, and how is it different from .unwrap_or(0)?
Show solution
.unwrap() panics, because parsing "abc" returns Err, and unwrap on an Err panics with the contained error. .unwrap_or(0) instead returns the fallback 0 without panicking. unwrap converts a recoverable error into an unrecoverable panic; unwrap_or recovers with a default.
Question #3
Write a function safe_divide(a: i32, b: i32) -> Result<i32, String> that returns Ok(a / b), or Err("division by zero") when b is 0. Then handle both cases of safe_divide(10, 0) with a match.
Show solution
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
fn main() {
match safe_divide(10, 0) {
Ok(result) => println!("result: {result}"),
Err(message) => println!("error: {message}"),
}
}error: division by zero
Returning Result lets safe_divide report the zero-divisor case as a value the caller handles, instead of panicking on the division (lesson 3.1). The match covers both variants, so the error can't be ignored.
Handling Result with match everywhere gets repetitive fast, especially when a function calls several fallible operations and wants to pass any failure up to its caller. The next lesson introduces the ? operator, which collapses that whole pattern into a single character.