In this lesson, you will learn how to replace cumbersome match statements with the concise ? operator to propagate errors upstream.
The Verbosity Problem
When handling operations that might fail, you have likely been using match to check if a Result is Ok or Err. If you find yourself nesting these matches—for example, reading a file, parsing its contents, and then processing a value—your code quickly becomes deeply indented and difficult to read. This is often called "the pyramid of doom."
Refactoring with the ? Operator
The ? operator is Rust’s elegant solution to this problem. When you place a ? after an expression that returns a Result, Rust performs a hidden check for you:
- If the result is
Ok(value), it unwraps the value and continues execution. - If the result is
Err(e), it immediately returns that error from your current function.
Consider this nested approach:
fn read_and_parse(path: &str) -> Result<i32, MyError> {
let content = match read_to_string(path) {
Ok(c) => c,
Err(e) => return Err(e.into()),
};
match content.trim().parse::<i32>() {
Ok(num) => Ok(num),
Err(e) => Err(e.into()),
}
}
By using the ? operator, we can flatten that entire logic into just two lines:
fn read_and_parse(path: &str) -> Result<i32, MyError> {
let content = read_to_string(path)?;
let num = content.trim().parse::<i32>()?;
Ok(num)
}
Why This Works
The ? operator doesn't just return the error; it automatically performs a conversion using the From trait. This means that if your function expects a specific error type, the ? operator will attempt to convert the underlying error into your function's error type, provided the conversion is defined. This keeps your business logic clean while maintaining full control over how errors flow through your program. You are no longer writing boilerplate; you are describing the "happy path" of your application.
Key Takeaway: The ? operator simplifies error handling by unwrapping Ok values or immediately returning Err values, allowing you to flatten nested code into a readable, linear flow.