Last updated: April 13, 2025
Table of Contents
- 1. Introduction: Robustness Through Explicit Handling
- 2. Categories of Errors in Rust
- 3. Unrecoverable Errors with
panic!
- 4. Recoverable Errors with
Result<T, E>
- 5. Propagating Errors with the
?
Operator - 6. Creating Custom Error Types (Briefly)
- 7. Conclusion: Choosing When to Panic
- 8. Additional Resources
1. Introduction: Robustness Through Explicit Handling
Unlike many languages that rely heavily on exceptions, Rust distinguishes between two main categories of
errors: recoverable
errors and unrecoverable
errors. This distinction forces
developers to think explicitly about how failure should be handled, leading to more robust and reliable
programs.
Rust provides different mechanisms for dealing with each category: the Result<T, E>
enum
for recoverable errors and the panic!
macro for unrecoverable errors. Understanding when and how
to use each is crucial for writing idiomatic Rust code.
If you're just beginning with Rust, ensure you've covered the basics in our Getting Started with Rust guide.
2. Categories of Errors in Rust
- Recoverable Errors: These are errors that are expected to happen occasionally and should
be handled gracefully by the program. Examples include a file not being found, network connection issues, or
invalid user input. Rust uses the
Result<T, E>
enum to represent these. - Unrecoverable Errors: These represent bugs or conditions from which the program cannot
realistically recover. Examples include accessing an array index out of bounds, encountering an unexpected
internal state, or hitting a situation that indicates a logic error in the code itself. Rust uses the
panic!
macro to signal these.
3. Unrecoverable Errors with panic!
When something goes irrecoverably wrong, the panic!
macro is used. Invoking panic!
stops the normal execution flow.
3.1 When to Use panic!
Panicking is appropriate when:
- A program reaches a state that shouldn't be possible (a bug).
- An external condition violates a fundamental contract the code relies on.
- You're writing example code, tests, or prototypes where robust error handling isn't the primary focus yet
(though using
unwrap()
orexpect()
is often preferred even here).
You generally **should not** panic for expected failures like user input errors or file-not-found scenarios.
Use Result
for those.
fn main() {
// Example: Explicit panic
// panic!("Something went terribly wrong!");
// Example: Implicit panic from array out-of-bounds access
let numbers = [1, 2, 3];
println!("Accessing index 5: {}", numbers[5]); // This will panic!
}
3.2 How panic!
Works
By default, when a panic occurs, Rust performs **stack unwinding**. It walks back up the call stack, cleaning up the data owned by each function it encounters. This cleanup process can be resource-intensive.
Alternatively, you can configure Rust to **abort** immediately upon panic (by adjusting settings in
Cargo.toml
). Aborting skips cleanup and hands control directly back to the operating system,
resulting in a smaller binary but potentially leaving resources in an unclean state.
4. Recoverable Errors with Result<T, E>
For errors that are expected and should be handled, Rust uses the Result
enum. It represents
either success (Ok
) containing a value, or failure (Err
) containing an error value.
4.1 The Result
Enum
Its definition is straightforward:
enum Result {
Ok(T), // Contains the success value of type T
Err(E), // Contains the error value of type E
}
Functions that might fail return a Result
. For example, File::open
returns
Result<std::fs::File, std::io::Error>
.
4.2 Handling Result
with match
The most fundamental way to handle a Result
is using a match
expression, forcing
you to consider both the Ok
and Err
cases:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let file_result = File::open("hello.txt");
let file_handle = match file_result {
Ok(file) => {
println!("File opened successfully.");
file // Return the file handle if Ok
},
Err(error) => match error.kind() {
// Handle specific error kinds
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => {
println!("File not found, created it instead.");
fc
},
Err(e) => panic!("Problem creating the file: {:?}", e),
},
// Handle other kinds of errors
other_error => {
panic!("Problem opening the file: {:?}", other_error);
}
},
};
// Use file_handle here...
}
Matching ensures you explicitly handle all possible outcomes.
4.3 Convenience Methods: unwrap()
and expect()
Result
provides shortcut methods, but use them cautiously:
unwrap()
: Returns the value insideOk
. If theResult
isErr
, it **panics** with a default error message. Convenient for examples or situations where failure is considered a bug, but risky in production code for expected errors.expect(message: &str)
: Similar tounwrap()
, but allows you to provide a custom panic message if theResult
isErr
. Slightly better thanunwrap()
as the message provides more context on panic.
// Using unwrap (can panic!)
// let file_handle = File::open("hello.txt").unwrap();
// Using expect (can panic, but with a custom message)
let file_handle = File::open("hello.txt")
.expect("Failed to open hello.txt");
Generally, prefer using match
or the ?
operator (see below) over
unwrap
/expect
for recoverable errors in library or application code.
5. Propagating Errors with the ?
Operator
Manually matching every Result
can be verbose. The ?
operator provides a concise
way to **propagate** errors up the call stack.
If used on a Result
value within a function that *itself* returns a Result
:
- If the value is
Ok(T)
, theT
inside is extracted and assigned to the variable. - If the value is
Err(E)
, theErr(E)
is **returned immediately** from the *entire function*. The error typeE
must be compatible with the function's declared error return type (often involving theFrom
trait for conversions).
use std::fs::File;
use std::io::{self, Read};
// This function returns Result, so we can use ? inside it.
fn read_username_from_file() -> Result {
let mut username_file = File::open("username.txt")?; // If Err, returns Err from function
let mut username = String::new();
username_file.read_to_string(&mut username)?; // If Err, returns Err from function
Ok(username) // If both operations succeed, return Ok(username)
}
// Note: The ? operator can only be used in functions that return Result or Option.
fn main() {
match read_username_from_file() {
Ok(username) => println!("Username: {}", username),
Err(e) => println!("Error reading username: {}", e),
}
}
The ?
operator significantly cleans up error handling code where errors need to be passed
upwards.
6. Creating Custom Error Types (Briefly)
For more complex applications or libraries, it's often beneficial to define custom error types (usually
structs or enums) that implement the std::error::Error
trait. This allows you to provide more
specific information about failures and integrate well with the ?
operator and other
error-handling libraries (like thiserror
or anyhow
).
7. Conclusion: Choosing When to Panic
Rust's error handling philosophy encourages developers to anticipate and handle potential failures explicitly
using the Result
enum. The panic!
macro is reserved for truly exceptional,
unrecoverable situations, often indicating a bug in the program's logic.
- Use
Result<T, E>
for expected, recoverable errors (file not found, network issues, invalid input). Handle them usingmatch
or propagate them using the?
operator. - Use
panic!
(or methods likeunwrap
/expect
that call panic) primarily for programming errors or unrecoverable states where continuing execution is impossible or unsafe.
By distinguishing these cases and leveraging Rust's type system, you can build more robust and reliable software.