Error Propagation
The ? operator is Rust's way of propagating errors
automatically. Instead of writing verbose match expressions to
handle every Result, you can use ? to automatically
return errors to the caller. This makes error handling much more concise and
readable.
What is the ? Operator?
The ? operator is a shorthand for propagating errors. When placed
after a Result value, it:
- If the value is
Ok, it unwraps the value and continues - If the value is
Err, it returns the error from the function
Before: Verbose Error Handling
Without the ? operator, you need to write verbose match
expressions:
use std::fs::File;
use std::io::Read;
fn read_username_from_file() -> Result<String, std::io::Error> {
let mut f = match File::open("hello.txt") {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
After: Using the ? Operator
With the ? operator, the same code becomes much more concise:
use std::fs::File;
use std::io::Read;
fn read_username_from_file() -> Result<String, std::io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
? operator reduces boilerplate
code significantly, making error handling more readable and maintainable.
Click Run to execute your code
How ? Works
The ? operator is syntactic sugar for:
match result {
Ok(value) => value,
Err(error) => return Err(error.into()),
}
Notice that it uses error.into(), which means it can convert between
compatible error types automatically!
? operator uses
Into trait to convert error types, allowing you to use different
error types in the same function chain.
Using ? with Different Error Types
You can use ? with different error types by using
Box<dyn std::error::Error>:
use std::fs::File;
use std::io::Read;
fn parse_and_read() -> Result<i32, Box<dyn std::error::Error>> {
let mut s = String::new();
File::open("number.txt")?
.read_to_string(&mut s)?;
let num: i32 = s.trim().parse()?;
Ok(num)
}
Error trait, allowing you to mix different error
types in one function.
Chaining Operations with ?
You can chain multiple operations that return Result:
fn read_file_contents(filename: &str) -> Result<String, std::io::Error> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Or even more concisely:
fn read_and_parse(filename: &str) -> Result<i32, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string(filename)?;
let number: i32 = contents.trim().parse()?;
Ok(number)
}
Click Run to execute your code
Using ? in main()
You can use ? in main() by changing its return type:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let username = read_username_from_file()?;
println!("Username: {}", username);
Ok(())
}
main() returns
Result, Rust will print the error and exit with a non-zero status
code if an error occurs.
When Can You Use ?
The ? operator can only be used in functions that return
Result (or Option). It cannot be used in functions
that return other types.
// โ
OK - Function returns Result
fn read_file() -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string("file.txt")?;
Ok(contents)
}
// โ Error - Function doesn't return Result
fn read_file() -> String {
let contents = std::fs::read_to_string("file.txt")?; // Error!
contents
}
Converting Error Types
The ? operator automatically converts error types using the
Into trait:
use std::num::ParseIntError;
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
s.parse()
}
fn read_and_parse() -> Result<i32, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string("number.txt")?; // io::Error
let number = contents.trim().parse()?; // ParseIntError
Ok(number)
}
io::Error and
ParseIntError can be converted to Box<dyn Error>
automatically, allowing you to use ? with different error types.
Best Practices
- Use ? for error propagation: Prefer ? over verbose match expressions
- Use Box<dyn Error> for mixed errors: When combining different error types
- Keep functions focused: Let errors propagate to the caller
- Add context when needed: Use map_err() before ? to add context
- Use ? in main(): Change main() return type to Result for cleaner error handling
- Document error conditions: Make it clear what errors your function can return
Common Mistakes
1. Using ? in functions that don't return Result
// Wrong - Function doesn't return Result
fn read_file() -> String {
let contents = std::fs::read_to_string("file.txt")?; // Error!
contents
}
// Correct - Function returns Result
fn read_file() -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string("file.txt")?;
Ok(contents)
}
2. Not handling incompatible error types
// Wrong - Different error types
fn process() -> Result<i32, std::io::Error> {
let num: i32 = "5".parse()?; // Error! ParseIntError != io::Error
Ok(num)
}
// Correct - Use Box<dyn Error>
fn process() -> Result<i32, Box<dyn std::error::Error>> {
let num: i32 = "5".parse()?;
Ok(num)
}
3. Forgetting to return Ok() at the end
// Wrong - Missing Ok()
fn read_file() -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string("file.txt")?;
contents // Error! Should be Ok(contents)
}
// Correct - Return Ok()
fn read_file() -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string("file.txt")?;
Ok(contents)
}
Exercise: Error Propagation Practice
Task: Implement functions using the ? operator for error propagation.
Requirements:
- Read and parse numbers from files
- Chain multiple operations with ?
- Handle different error types
- Use Box<dyn Error> for mixed error types
Click Run to execute your code
Show Solution
use std::fs::File;
use std::io::Read;
fn read_number_from_file(filename: &str) -> Result<i32, Box<dyn std::error::Error>> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let number: i32 = contents.trim().parse()?;
Ok(number)
}
fn read_divide(filename: &str) -> Result<f64, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string(filename)?;
let divisor: i32 = contents.trim().parse()?;
if divisor == 0 {
return Err("Division by zero".into());
}
Ok(100.0 / divisor as f64)
}
fn validate_and_divide(n: i32) -> Result<f64, String> {
if n <= 0 {
return Err(String::from("Number must be positive"));
}
if n == 0 {
return Err(String::from("Division by zero"));
}
Ok(100.0 / n as f64)
}
Summary
- ? operator propagates errors automatically
- Can only be used in functions that return
ResultorOption - Unwraps
Okvalues, returnsErrvalues - Automatically converts error types using
Intotrait - Use
Box<dyn Error>for mixed error types - Much more concise than verbose
matchexpressions - Can be used in
main()by changing return type - Chains multiple operations cleanly
- Remember to return
Ok()at the end
What's Next?
Now that you understand error propagation, you'll learn about custom error types. Creating your own error types allows you to provide better error messages, add context, and make your error handling more structured and maintainable.
Enjoying these tutorials?