Rust Error Handling Explained: Result, the ? Operator, panic! and expect
November 10, 2026
Welcome to episode 14 of Rust for Beginners, a series for complete beginners (no programming experience needed). In the last video, we learned about Option. Today, we'll learn how Rust handles errors: panic, Result, and the question mark operator. Every code block below is the exact code from the video, ready to copy.
- All episodes are in the Rust Tutorial for Beginners playlist.
- Versions used: Rust 1.99.1 on Ubuntu, VS Code with rust-analyzer.
1. New project
Let's create a new project called errors, and open it in VS Code.
cargo new errors
cd errors2. Two kinds
Rust has two kinds of errors. Some are bugs the program can't recover from. Those panic. Others are expected, like bad user input. Those return a Result.
3. panic!
panic stops the program right away, with a message.
src/main.rs:
fn main() {
println!("Starting...");
panic!("Something went terribly wrong!");
}Output (the thread number changes every run):
Starting...
thread 'main' (3533) panicked at src/main.rs:3:5:
Something went terribly wrong!
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace4. Run A
Let's run it. We see "Starting", then the panic message, and the line where it happened.
cargo run5. Result
Result is an enum, like Option. It's either Ok, with a value, or Err, with an error.
6. parse
parse turns text into a number. It can fail, so it returns a Result. We match on it, just like an Option.
fn main() {
for text in ["42", "abc"] {
let result: Result<i32, _> = text.parse();
match result {
Ok(number) => println!("{text} -> {number}"),
Err(error) => println!("{text} -> error: {error}"),
}
}
}Output: 42 -> 42 / abc -> error: invalid digit found in string
7. Run B
Let's run it. Forty-two works. "abc" gives an error: invalid digit. And no crash.
cargo run8. Our own
Our own functions can return a Result too. check age returns the age, or an error message as a String.
fn check_age(text: &str) -> Result<u32, String> {
let age: u32 = match text.parse() {
Ok(n) => n,
Err(_) => return Err(format!("'{text}' is not a number")),
};
if age > 150 {
return Err(format!("{age} is too old"));
}
Ok(age)
}
fn main() {
for text in ["25", "abc", "200"] {
match check_age(text) {
Ok(age) => println!("Age: {age}"),
Err(message) => println!("Error: {message}"),
}
}
}Output: Age: 25 / Error: 'abc' is not a number / Error: 200 is too old
9. Err / Ok
When something's wrong, we return Err. At the end, we wrap the good value in Ok.
10. Run C
Let's run it. Twenty-five is fine, "abc" is not a number, and two hundred is too old.
cargo run11. ?
Writing match every time gets long. The question mark does it for us. If parse fails, the question mark returns the error from the function right away. If it works, we get the number.
use std::num::ParseIntError;
fn add_strings(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x: i32 = a.parse()?;
let y: i32 = b.parse()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strings("2", "3"));
println!("{:?}", add_strings("2", "three"));
let n: i32 = "seven".parse().expect("Please give me a number");
println!("{n}");
}Output:
Ok(5)
Err(ParseIntError { kind: InvalidDigit })
thread 'main' (3661) panicked at src/main.rs:13:34:
Please give me a number: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace12. Rule
The question mark only works in a function that returns a Result itself.
13. expect
And expect is like unwrap: it gives the value or panics, but with our own message.
14. Run D
Let's run it. Two plus three is Ok five. "three" isn't a number, so we get the Err. And expect panics with our message.
cargo runRecap
To recap: panic for bugs you can't recover from. Result, with Ok and Err, for errors you expect. The question mark passes errors up. And expect or unwrap only when failing is truly a bug.