Rust's Question Mark Operator (?): Propagate Errors, Not Boilerplate
The `?` operator cleans up Rust error handling by propagating `Err` values. Instead of a verbose `match` block, you append `?` to a `Result` or `Option`, and it automatically returns the error if present, letting you focus on the happy path.
WHY IT EXISTS: Early Rust code for error handling was verbose. Functions that could fail often involved deeply nested match statements or chains of .unwrap() calls. This either cluttered the logic with boilerplate or introduced the risk of panics, hiding the "happy path" of the code. A cleaner, built-in way was needed to handle the common pattern of "if this operation fails, stop and pass the failure up to the caller."
THE MENTAL MODEL: Think of the question mark operator as a conditional early return for errors. It asks, "Is this value an error?" If the answer is yes, it immediately returns that error from the current function. If the answer is no, it unwraps the successful value and lets the code proceed. It's a direct replacement for the older try! macro, which itself was a shortcut for a full match block.
HOW IT WORKS: When you append ? to an expression that evaluates to a Result<T, E> or Option<T>, the compiler replaces it with more verbose logic. If the value is Ok(v) or Some(v), the entire expression? evaluates to just v. If the value is Err(e), the ? operator triggers an immediate return Err(e.into()) from the enclosing function. The .into() call is a key feature, as it attempts to convert the error type into the one expected by the function's return signature, allowing for more flexible error composition. For an Option, encountering None causes an early return of None.
WHEN TO USE IT: Use ? inside any function that returns a Result or Option. It is ideal for chaining multiple operations that could each fail, such as reading from a file, parsing its contents, and then performing a calculation. The operator keeps the success-path code clean, linear, and easy to read by handling the error path implicitly.
WHEN NOT TO USE IT: The biggest footgun is attempting to use ? in a function that does not return a Result or Option, like the main function in older Rust editions or any function returning a simple value or (). The compiler will prevent this. Also, avoid ? when you need to handle an error specifically at that point instead of just propagating it. If you need to retry, log, or provide a default value, a match statement or a method like .unwrap_or_default() is more appropriate.
ONE CANONICAL EXAMPLE: Consider a function to read a user's ID from a file, which might not exist or might contain invalid data. fn get_user_id_from_file() -> Result<i32, std::io::Error> { let content = std::fs::read_to_string("user_id.txt")?; let id = content.trim().parse().map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; Ok(id) } The first ? handles a potential file reading error. The second ? handles a potential parsing error, which we map into an io::Error to match the function's signature before propagating.
Read the original → doc.rust-lang.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.