Rust's error handling is marked by its capability to handle errors at compile-time without throwing exceptions. This article will delve into the basics of error handling in Rust, offering examples and best practices to guide you on your journey.
Types of Errors
In Rust, errors are classified into two primary types: recoverable and unrecoverable errors.
- Recoverable errors: These are errors that your program can recover from after it encounters them. For instance, if your program attempts to open a file that doesn't exist, it is a recoverable error because your program can then proceed to create the file. Rust represents recoverable errors with the
Result<T, E>enum. - Unrecoverable errors: These are errors that the program cannot recover from, causing it to stop execution. Examples include memory corruption or accessing a location beyond an array's boundaries. Rust represents unrecoverable errors with the
panic!macro.
Unrecoverable Errors with panic!
When your program encounters an unrecoverable error, the panic! macro is used. This macro stops the program immediately, unwinding and cleaning up the stack. Here's a simple example:
fn main() {
panic!("crash and burn");
}
When this program runs, it will print the message "crash and burn", unwind, clean up the stack, and then quit.
Recoverable Errors with Result<T, E>
For recoverable errors, Rust uses the Result<T, E> enum. This enum has two variants: Ok(value), which indicates that the operation was successful and contains the resulting value and Err(why)an explanation of why the operation failed.
For instance, here's a function that attempts to divide two numbers, returning a Result:
fn divide(numerator: f64, denominator: f64) -> Result<f64, &'static str> {
if denominator == 0.0 {
Err("Cannot divide by zero")
} else {
Ok(numerator / denominator)
}
}
When calling this function, you can use pattern matching to handle the Result:
match divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(err) => println!("Error: {}", err),
}
The Question Mark Operator
Rust has a convenient shorthand for propagating errors: the ? operator. If the value of the Result is Ok, the ? operator unwraps the value and gives it. If the value is Err, it returns from the function and gives the error.
Here's an example:
fn read_file(file_name: &str) -> Result<String, std::io::Error> {
let mut f = File::open(file_name)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
In this function, if File::open or f.read_to_string encounters an error, the function will immediately return that error. If not, the function will eventually replace the file's contents.
Best Practices
Here are some best practices for error handling in Rust:
- Use
Result<T, E>for recoverable errors: If there's a chance your function might fail, it should return aResult. This allows the calling code to handle the failure case explicitly. - Leverage the
?operator: Remember to use the operator for error propagation if you're writing a function that returns aResult. It makes your code cleaner and easier to read. - Make use of the
unwrap()orexpect()methods sparingly: These methods will cause your program to panic if they're called on aErrvariant. It's generally better to handle errors gracefully withmatchthe?operator. - Don't panic!: Reserve
panic!for situations when your code is in a state, it can't recover from. If there's any chance of recovery, return aResultinstead. - Customize error types: Rust allows you to define your own error types, which can give more meaningful error information. This can be particularly useful in more extensive programs and libraries.
- Handle all possible cases: When dealing with
Resulttypes, make sure your code handles both theOkandErrvariants. Rust's exhaustive pattern matching will remind you of this, but it's a good principle.
Here's an example of creating a custom error:
use std::fmt;
#[derive(Debug)]
struct MyError {
details: String
}
impl MyError {
fn new(msg: &str) -> MyError {
MyError{details: msg.to_string()}
}
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}


