Error handling is one of the biggest differences between Rust and many other programming languages. Instead of exceptions, Rust relies on the Result<T, E> type, forcing developers to explicitly deal with operations that may fail.
If you are learning Rust, you have probably encountered code such as:
let file = File::open("config.txt")?;
or:
let file = File::open("config.txt").expect("Config file missing");
or even:
match File::open("config.txt") {
Ok(file) => file,
Err(error) => panic!("{:?}", error),
}
At first glance, these approaches may appear similar, but they solve different problems.
In this guide, you’ll learn exactly when to use match, when the ? operator is the better choice, and when expect() makes sense.
Understanding Result in Rust
Many operations in Rust can fail. Opening a file, reading data, parsing numbers, making network requests, or connecting to a database may all produce errors.
Rust represents these operations using the Result enum:
enum Result<T, E> { Ok(T), Err(E), }
A successful operation returns Ok, while a failure returns Err.
use std::fs::File;
let file = File::open("config.txt");
The variable above has the following type:
Result<File, std::io::Error>
You must decide how to handle both possibilities.
Example Scenario
Let’s assume our application needs to open a file named config.txt.
We will solve the same problem using all three approaches.
Using expect()
use std::fs::File;
fn main() {
let file = File::open("config.txt").expect("Configuration file not found");
println!("{:?}", file);
}
The behavior is straightforward:
- If the file exists, the program continues.
- If the file does not exist, the program immediately panics.
Typical output:
thread 'main' panicked at: Configuration file not found
When should you use expect()?
- Configuration files that absolutely must exist.
- Environment variables required for startup.
- Examples and tutorials.
- Unit tests.
- Situations where continuing execution makes no sense.
The biggest advantage of expect() is that it provides a custom error message, making debugging easier.
unwrap() vs expect()
Both unwrap() and expect() extract the successful value from a Result. If the operation fails, both methods panic and immediately terminate the program.
The difference lies in the error message.
Using unwrap()
let file = File::open("config.txt").unwrap();
If the file cannot be opened, Rust produces a default panic message:
thread 'main' panicked at: called `Result::unwrap()` on an `Err` value
Using expect()
let file = File::open("config.txt") .expect("Configuration file not found");
If the operation fails, your custom message is displayed together with the original error.
thread 'main' panicked at: Configuration file not found: No such file or directory
Because of the additional context, many Rust developers prefer expect() over unwrap().
Using match
use std::fs::File;
fn main() {
let file = match File::open("config.txt") {
Ok(file) => file,
Err(error) => { panic!("Problem opening file: {:?}", error);}
};
println!("{:?}", file); }
This code behaves similarly to expect(), but gives you complete control over both cases.
You are not forced to panic. For example
match File::open("config.txt") {
Ok(file) => file,
Err(_) => { println!("Creating default configuration...");
File::create("config.txt").unwrap()
}
}
This flexibility makes match one of the most powerful tools in Rust.
When should you use match?
- When success and failure require different logic.
- When you want to recover from an error.
- When different errors require different actions.
- When you need maximum control.
Using the ? Operator
use std::fs::File;
use std::io;
fn load_config() -> io::Result<File> {
let file = File::open("config.txt")?;
Ok(file)
}
The ? operator works differently.
If the operation succeeds, the value is extracted.
If the operation fails, the function immediately returns the error to its caller.
The operator is roughly equivalent to:
let file = match File::open("config.txt") {
Ok(file) => file,
Err(error) => return Err(error),
};
No panic occurs.
No error handling happens locally.
The current function simply says: “I cannot continue, so I will let my caller decide.”
Real-World Example with ?
use std::fs;
fn read_configuration() -> Result<String, std::io::Error> {
let contents = fs::read_to_string("config.txt")?;
Ok(contents)
}
fn main() {
match read_configuration() {
Ok(config) => println!("{}", config),
Err(error) => println!("Could not read configuration: {}", error),
}
}
Here:
read_configuration()propagates errors.main()decides what to do.
expect() vs match vs ?
| Approach | Behavior | Panics? | Best Use Case |
|---|---|---|---|
| expect() | Stops execution | Yes | Fatal errors |
| match | Custom handling | Optional | Recovery logic |
| ? | Propagates error | No | Application code |
Why Rust Developers Prefer ?
Modern Rust code heavily relies on the ? operator.
Without it, functions become filled with repetitive matches:
let config = match read_config() {
Ok(c) => c,
Err(e) => return Err(e),
};
With the operator:
let config = read_config()?;
The code becomes easier to read and significantly shorter.
Where Should Errors Be Handled?
A common pattern in Rust applications is:
- Low-level functions use
?. - Business logic propagates errors upward.
- The top-level application decides how to display errors.
Example:
fn load_file() -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string("data.txt")?;
Ok(contents)
}
fn run() -> Result<(), std::io::Error> {
let data = load_file()?;
println!("{}", data);
Ok(())
}
fn main() {
if let Err(error) = run() {
println!("Application error: {}", error); }
}
This keeps error handling centralized and avoids unnecessary panics.
Common Beginner Mistakes
Using unwrap() everywhere
Many beginners write:
let file = File::open("config.txt").unwrap();
This causes a panic without providing meaningful context.
Using match for everything
While match is powerful, using it for simple propagation often creates unnecessary boilerplate.
Panicking in library code
Libraries should generally return errors rather than terminating the application.
Recommended Guidelines
- Use
?for most functions. - Use
matchwhen different actions are required. - Use
expect()for unrecoverable situations. - Avoid
unwrap()in production code.
Final Thoughts
There is no single “correct” way to handle errors in Rust. Each approach exists for a specific purpose.
- match gives you complete control over both success and failure cases.
- ? allows you to propagate errors cleanly and keep your code concise.
- expect() is useful when the application cannot continue without a particular resource.
- unwrap() can be convenient for quick experiments, tests, and prototypes.
As your Rust applications grow, you will likely find yourself using the ? operator most of the time, reserving match for custom recovery logic and expect() for unrecoverable situations.
Understanding when to use each of these approaches is one of the key steps toward writing clean, idiomatic, and maintainable Rust code.
What About You?
Error handling is one of the most discussed topics in the Rust community, and every developer eventually develops their own preferences.
Do you primarily use the ? operator? Do you prefer explicit match statements? Do you avoid unwrap() entirely, or do you still use it in certain situations?
Leave a comment below and share how you handle errors in your Rust projects. It would be interesting to hear which approach you use most often and why.