Error Handling in Rust
Rust groups errors into two categories: recoverable errors (like file not found) and unrecoverable errors (like accessing an array out of bounds). Instead of exceptions, Rust uses Result<T, E> for recoverable errors and panic! for unrecoverable ones.
Unrecoverable Errors with panic!
When something goes terribly wrong and there's no way to recover:
fn main() {
// This will panic and stop the program
panic!("crash and burn!");
}
Common situations that cause panics:
fn main() {
let v = vec![1, 2, 3];
// This will panic: index out of bounds
// v[99]; // Uncommenting this will crash
println!("This won't print if we panic above");
}
The Result Enum
For recoverable errors, Rust uses the Result type:
enum Result<T, E> {
Ok(T), // Success, contains the value
Err(E), // Error, contains error info
}
Basic Usage
use std::fs::File;
fn main() {
let file_result = File::open("hello.txt");
let file = match file_result {
Ok(f) => f,
Err(e) => {
println!("Failed to open file: {}", e);
return;
}
};
println!("File opened successfully!");
}
Handling Different Error Types
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let file = File::open("hello.txt");
let file = match file {
Ok(f) => f,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
println!("File not found, creating it...");
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Couldn't create file: {:?}", e),
}
}
other_error => {
panic!("Problem opening file: {:?}", other_error);
}
},
};
}
Shortcuts: unwrap and expect
For prototyping or when you're sure an operation will succeed:
use std::fs::File;
fn main() {
// unwrap: panics with generic message if Err
// let f = File::open("hello.txt").unwrap();
// expect: panics with your custom message if Err
let f = File::open("hello.txt")
.expect("Failed to open hello.txt");
}
Propagating Errors
Often you want to return errors to the caller:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let file = File::open("username.txt");
let mut file = match file {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut username = String::new();
match file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
fn main() {
match read_username_from_file() {
Ok(name) => println!("Username: {}", name),
Err(e) => println!("Error reading username: {}", e),
}
}
The ? Operator
The ? operator provides a concise way to propagate errors:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut file = File::open("username.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
Ok(username)
}
// Even more concise with chaining:
fn read_username_short() -> Result<String, io::Error> {
let mut username = String::new();
File::open("username.txt")?.read_to_string(&mut username)?;
Ok(username)
}
fn main() {
match read_username_from_file() {
Ok(name) => println!("Username: {}", name),
Err(e) => println!("Error: {}", e),
}
}
Using ? with Option
The ? operator also works with Option:
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
fn main() {
let text = "Hello\nWorld";
match last_char_of_first_line(text) {
Some(c) => println!("Last char: {}", c),
None => println!("No character found"),
}
}
Creating Custom Errors
You can define your own error types:
use std::fmt;
#[derive(Debug)]
struct ValidationError {
message: String,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Validation error: {}", self.message)
}
}
fn validate_age(age: i32) -> Result<(), ValidationError> {
if age < 0 {
Err(ValidationError {
message: String::from("Age cannot be negative"),
})
} else if age > 150 {
Err(ValidationError {
message: String::from("Age seems unrealistic"),
})
} else {
Ok(())
}
}
fn main() {
match validate_age(25) {
Ok(()) => println!("Age is valid"),
Err(e) => println!("{}", e),
}
match validate_age(-5) {
Ok(()) => println!("Age is valid"),
Err(e) => println!("{}", e),
}
}
When to Use panic! vs Result
Use panic! when:
- The error is unrecoverable (bug in code, corrupted data)
- In examples, prototypes, or tests
- When you're absolutely sure the code won't fail
Use Result when:
- The error is expected and recoverable
- The caller should decide how to handle it
- In library code (let users decide)
Practice Exercise
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Cannot divide by zero"))
} else {
Ok(a / b)
}
}
fn main() {
// Try different values
let calculations = vec![
(10.0, 2.0),
(10.0, 0.0),
(100.0, 4.0),
];
for (a, b) in calculations {
match divide(a, b) {
Ok(result) => println!("{} / {} = {}", a, b, result),
Err(e) => println!("{} / {} failed: {}", a, b, e),
}
}
}
Key Takeaways
- Rust has no exceptions - use
Resultandpanic! panic!for unrecoverable errors that crash the programResult<T, E>for recoverable errors withOk(T)orErr(E)- Use
unwrap()andexpect()for quick prototyping - The
?operator propagates errors concisely - Prefer
Resultin library code to give callers control - Create custom error types for domain-specific errors
Proper error handling makes your Rust programs robust and reliable!
Next Steps
Now that you understand error handling, you're ready to learn about collections — the vectors, hash maps, and sets Rust uses to store and manage groups of values. Many of their operations return Option, so the handling patterns you just learned will feel right at home.
Next lesson
Collections
Learn to use Rust's standard collections including Vec, HashMap, and HashSet with iterators
25 min