Rust Error Handling Explained: When to Use match, ?, and expect()

June 28, 2026
Error Handling,Rust Learning

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 match when 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.

Leave a Comment

Custom Web Development Services