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!