In many programming languages, errors are treated as an afterthought. If a file is missing or a network request fails, the language throws an “Exception,” abruptly halting the normal flow of the program. If you forget to wrap that code in a try/catch block, your entire application crashes.
Worse still is the billion-dollar mistake: the null value. Trying to access a value that happens to be null is a leading cause of software crashes worldwide.
Rust takes a radically different approach. Rust does not have Exceptions, and it does not have null. Instead, it treats errors and missing values as standard data, using two incredibly powerful Enums built right into the standard library: Option and Result.
Let’s create a new project to see how this prevents crashes:
cargo new error_handling
cd error_handling
Handling “Nothing” with the Option Enum
How do you represent a value that might be absent? For example, if you ask a program to find the middle name of a user, they might have one, or they might not.
Instead of returning null, Rust returns an Option. The Option enum has exactly two variants:
Some(value): Contains the actual data.None: Represents the absence of data.
Open your src/main.rs and let’s write a function that looks for a specific user in a list:
fn find_user(id: u32) -> Option<String> {
if id == 1 {
// We found a user! Wrap the data in 'Some'
Some(String::from("Alice"))
} else {
// No user found. Return 'None'
None
}
}
fn main() {
let search_result = find_user(2);
// We use 'match' to safely handle both possibilities!
match search_result {
Some(name) => println!("User found: {}", name),
None => println!("User does not exist in the database."),
}
}
If you run cargo run, it will print: User does not exist in the database.
Because Option is an Enum, the compiler forces you to use a match statement (or similar logic) to handle both the Some and None cases before you are allowed to use the underlying string. You can never accidentally trigger a “null reference” crash because Rust forces you to acknowledge that the data might be missing.
Handling Failures with the Result Enum
While Option is for things that might be missing, the Result enum is for operations that might fail.
Whenever you do something risky like reading a file from the hard drive, parsing a string into a number, or connecting to a database, Rust functions return a Result.
The Result enum also has two variants:
Ok(value): The operation succeeded, and here is the data.Err(error): The operation failed, and here is why.
Let’s try to open a text file that doesn’t exist. Update your src/main.rs to include the standard filesystem library (std::fs):
use std::fs::File;
fn main() {
// Attempt to open a file that we haven't created
let file_result = File::open("config.txt");
match file_result {
Ok(file) => println!("File opened successfully: {:?}", file),
Err(error) => println!("Failed to open the file! Reason: {}", error),
}
}
Run cargo run and watch what happens:
Failed to open the file! Reason: The system cannot find the file specified. (os error 2)
The program didn’t crash. It didn’t throw an unhandled exception. It simply fell into the Err arm of our match statement and printed a polite message. You are fully in control of the failure.
Shortcuts for Prototyping: unwrap and expect
Using a match statement for every single Option and Result can feel tedious when you are just quickly prototyping an idea or writing a small script.
Rust provides a few helper methods to speed this up, though they come with a warning: they will crash your program if they fail.
The .unwrap() Method
If you call .unwrap() on a Result, it will give you the inner data if it is Ok. If it is an Err, it will instantly panic and crash the program.
use std::fs::File;
fn main() {
// If config.txt is missing, the program instantly crashes here.
let _file = File::open("config.txt").unwrap();
}
The .expect() Method
This is exactly like .unwrap(), but it allows you to provide a custom error message before the program crashes. This is highly recommended over unwrap because it makes debugging much easier.
use std::fs::File;
fn main() {
// If it fails, it prints our custom message right before crashing.
let _file = File::open("config.txt").expect("CRITICAL: Missing config.txt file!");
}
When to use them: Use .expect() when writing tests, prototyping, or in situations where a missing file should legitimately kill the program (for example, if a web server cannot load its security certificates, it is better to crash immediately than to run insecurely).
For everything else, especially user-facing logic stick to match to handle errors gracefully.
Summary
Rust’s error handling philosophy is built on predictability:
Use
Option(Some/None) when a value might simply be missing.Use
Result(Ok/Err) when an operation might explicitly fail.Use
matchto force yourself to handle every possible outcome safely.Use
.expect()only when a failure means the program fundamentally cannot continue.
By making errors a standard part of the type system, Rust ensures that your production applications are incredibly resilient. You handle the worst-case scenarios while you are writing the code, not at 3:00 AM when a server crashes.
Learning Rust
Continue your journey with the next step or visit a previous post you may have missed
Rust: Setting up your development environment
Rust: Cargo and the Anatomy of “Hello, World”
Rust: Variables and Immutability
Rust: Control Flow, Making Decisions with If, Else, and Loops
Rust: Ownership Explained, Memory Safety
Rust: Structs and Enums, Modeling Custom Data
Rust: Pattern Matching, Mastering the Match Operator
Rust: Error Handling, Ditching Exceptions for Result and Option
Rust: Collections, Mastering Vectors and Strings
Rust: Modules and Traits, Organizing Code and Shared Behavior