
Rust Code Examples: Error Handling with `?` and `Result`
Understanding Result and the ? Operator
The Result<T, E> type in Rust is an enum that represents either a successful value (Ok(T)) or an error (Err(E)). When writing functions that may fail, returning a Result allows the caller to explicitly handle both success and failure cases.
The ? operator provides a concise way to propagate errors from a function. If a function returns a Result, using ? on a Result value will return the Err variant early if one exists, otherwise it will unwrap and return the Ok variant.
Example: Simple File Reading
use std::fs::File;
use std::io::{self, Read};
fn read_file_contents(path: &str) -> io::Result<String> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}In this example, File::open and file.read_to_string both return a Result. The ? operator is used to propagate any errors that may occur during these operations.
Custom Error Types
Using a custom error type can improve clarity and flexibility, especially in larger projects. Rust provides the thiserror crate to simplify the definition of error types.
Example: Custom Error with thiserror
Add thiserror to your Cargo.toml:
[dependencies]
thiserror = "1.0"Then define and use a custom error type:
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
enum AppError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Invalid input: {0}")]
InvalidInput(String),
}
fn process_input(input: &str) -> Result<(), AppError> {
if input.is_empty() {
return Err(AppError::InvalidInput("Input is empty".to_string()));
}
// Simulate some processing
Ok(())
}This example demonstrates how to create a custom error type and convert standard library errors into it using the #[from] attribute.
Combining Results and Errors
When working with multiple operations that can fail, you can combine Result values using the ? operator or using map and and_then.
Example: Combining Multiple Operations
use std::fs::File;
use std::io::{self, Read};
fn read_and_process(path: &str) -> io::Result<String> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
if contents.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Empty file content"));
}
Ok(contents)
}In this example, if any of the operations fail or the file is empty, an appropriate error is returned.
Comparing Error Handling Approaches
| Approach | Description | Pros | Cons |
|---|---|---|---|
? operator | Propagates errors early in a function | Clean, concise | Only works in Result/Option functions |
match expression | Explicitly matches Ok and Err variants | Full control over each case | Verbose |
unwrap() / expect() | Unwraps Result, panics on error | Quick for simple cases | Not safe for production code |
| Custom error types | Combines multiple error sources into a single type | Clear, flexible, composable | Requires additional setup |
Best Practices for Error Handling
- Prefer
?overmatchwhen the logic is simple and you want to avoid boilerplate. - Use custom error types in larger projects to make error handling consistent and meaningful.
- Avoid
unwrap()in production code; it can lead to panics and crashes. - Document error conditions in function documentation to inform users about possible failure scenarios.
Real-World Example: JSON Parsing with Error Handling
use std::fs::File;
use std::io::Read;
use serde_json::Value;
use thiserror::Error;
#[derive(Error, Debug)]
enum JsonError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
}
fn read_json_file(path: &str) -> Result<Value, JsonError> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let value: Value = serde_json::from_str(&contents)?;
Ok(value)
}This example shows how to handle both I/O and JSON parsing errors using a custom error type and the ? operator.
