panic! and Result are two mechanisms to represent failures, either unrecoverable or manageable.
In many modern languages, when something goes wrong an exception is thrown. The flow jumps until it finds a catch block, and if nobody catches it, it propagates all the way to the runtime.
Unchecked exceptions don’t always appear in the signature. Looking at a function like public void process(), it can be hard to know what failures it propagates without reviewing its implementation or documentation.
Rust takes a different path. It divides errors into two fundamental categories:
- Panics (
panic!): The code has reached a state where it cannot reasonably continue, often due to a bug or a violated precondition. - Recoverable errors (
Result<T, E>): Expected situations (file not found, network failure) that the caller can handle.
Unrecoverable errors: panic!
A panic occurs when the code detects a bug. For example:
- Trying to access index 10 of a 5-element array.
- Dividing an integer by zero.
- Explicitly calling the
panic!()macro.
When a panic occurs, the default configuration starts a process called unwinding. Rust walks the stack backwards and destroys the values of each frame before terminating the thread that panicked. You can also configure panic = "abort" to abort without unwinding.
fn main() {
// This is an explicit panic
panic!("Something terrible has happened!");
}When you run it, you’ll see:
thread 'main' panicked at 'Something terrible has happened!', src/main.rs:2:5Backtrace
If you want to know exactly which calls led to the error, you can run your program with a special environment variable:
RUST_BACKTRACE=1 cargo run
When to use panic!?
Only when the program is broken.
- Don’t use
panic!if the user enters a wrong password. - It can make sense if the code detects that an essential internal configuration is missing or inconsistent and there is no reasonable way to continue.
Recoverable errors: Result<T, E>
Most errors are not bugs; they are real-life circumstances: a down server, a full disk, an incorrect date format. We don’t want to kill the program; we want to notify the user or retry.
For this, Rust uses the Result enum:
enum Result<T, E> {
Ok(T), // The operation succeeded and contains the value T
Err(E), // The operation failed and contains the error E
}Notice it’s very similar to Option. But while Option is “something or nothing”, Result is “success or failure”.
The classic example: Opening a file
The File::open function doesn’t return a File. It returns a Result<File, Error>.
use std::fs::File;
fn main() {
let f = File::open("hello.txt"); // f is of type Result<File, std::io::Error>
let file = match f {
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file: {:?}", error);
},
};
}You can’t use the file as if File::open directly returned a File. You first need to handle, transform, or propagate the Result.
Shortcuts to panics: unwrap and expect
Sometimes writing a match is too verbose, especially when you’re prototyping or testing and don’t mind the program crashing if something fails.
Rust provides methods on Result that extract the value if it’s Ok or panic if it’s Err.
unwrap()
“Give me the value or die.”
let f = File::open("hello.txt").unwrap();If the file exists, f will be the File. If not, the program will panic!.
expect(msg)
“Give me the value or die with this message.”
let f = File::open("hello.txt").expect("Failed to open hello.txt");It’s the same as unwrap, but it allows you to customize the panic error message. It’s much better for debugging.
Best practices:
In paths that can reasonably fail, avoid using unwrap(). Use expect() if the failure represents a broken invariant and you want to explain the reason, or return and handle the error when the caller can recover.
Propagating errors with ?
Often, we don’t want to handle the error in the current function, but rather pass it to the function that called us so it can decide what to do.
In other languages, you would throw (throw an exception). In Rust, we return the error.
Long form (with match):
use std::fs::File;
use std::io::{self, Read};
fn read_user() -> Result<String, io::Error> {
let f = File::open("user.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e), // <--- Return the error early
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e), // <--- Return the error
}
}This is tedious. Rust has a very convenient operator for this: the question mark ?.
Short form (with ?):
use std::fs::File;
use std::io::{self, Read};
fn read_user() -> Result<String, io::Error> {
// If it fails, return Err(e) immediately. If it succeeds, it gives us the File.
let mut f = File::open("user.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?; // If it fails, returns Err; otherwise, continues
Ok(s) // If we get here, everything went well
}The ? operator does three things:
- Looks at the
Result. - If it is
Ok, extracts the value and continues. - If it is
Err, exits the function and propagates the error, converting it when the return type allows it viaFrom.
We can even chain it:
fn read_user_short() -> Result<String, io::Error> {
let mut s = String::new();
File::open("user.txt")?.read_to_string(&mut s)?;
Ok(s)
}Summary
Error handling in Rust is explicit and safe.
panic!: For unrecoverable errors (bugs). The program stops.Result<T, E>: For recoverable errors. You must handle them.unwrap()/expect(): Extract the value or cause a panic. Use them with caution.?operator: Propagates the error upwards and can convert its type.
Now that we know how to propagate errors, a question arises: What happens if one function returns a database error and another returns a file-reading error? How do I return both from the same function?
In the next article, we’ll see Option, the type that represents the expected absence of a value. After that, we’ll return to Result to delve into its combinators and error types.