Rust’s `?` operator for concise error propagation
The `?` operator simplifies error handling in Rust by automatically propagating `Result` or `Option` errors from functions that return `Result`. This guide explains how it works, gives a concrete example, and highlights common pitfalls.
09 Jul 2026, 15:54 UTC

The problem: verbose error propagation
When writing functions that call multiple fallible operations, Rustaceans traditionally need to manually match on each result or chain if let statements. This quickly becomes boilerplate, especially when the only goal is to forward any error upward. Consider a function that parses a configuration string, converts it to an integer, and divides by it:
use std::num::ParseIntError;
fn process_config(raw: &str) -> Result<f64, String> {
let parsed = match raw.parse::() {
Ok(n) => n,
Err(_) => return Err("failed to parse integer".into()),
};
let result = match parsed / 2 {
Ok(q) => q,
Err(_) => return Err("division by zero in config".into()),
};
Ok(result as f64)
}
Every new fallible step adds three lines of match logic. The ? operator eliminates this pattern when the function’s return type already declares an error type.
The `?` operator in action
In any function declared to return Result<T, E>, the ? operator does two things on the value it follows:
- If the value is
Ok(x), it extractsxand the computation continues. - If the value is
Err(e), it immediately returnsErr(e)from the enclosing function, as if you had writtenreturn Err(e).
This works because the expression’s error type E must implement the Try trait (or have a From implementation that can convert it into the function’s error type). When both conditions are met, the compiler silently inserts the propagation logic.
How it works under the hood
The ? operator is desugared by the compiler into a match expression that checks the result’s variant and either unwraps or returns the error. The key requirement is that the Try trait be in scope or implemented for the error type. In practice, this means:
Result<T, E> ?works whenEcan be converted into the function’s declared error type viaFrom::from.Option<T> ?works when the function’s error type implementsFrom<Option<E>>, convertingNoneinto an appropriate error.
No edition changes affect the operator’s semantics since it was stabilized in Rust 1.0, but the Try trait must be available in the module’s scope.
A concrete example
The following example shows a function that chains two fallible operations using ?. The error type ParseIntError is automatically converted into the function’s declared error type String because String implements From<ParseIntError>.
use std::num::ParseIntError;
fn parse_config_int(s: &str) -> Result<i32, String> {
let value = s.parse::()?; // ? propagates ParseIntError as String
let doubled = value * 2;
Ok(doubled)
}
fn main() {
let ok = parse_config_int("42");
// ok == Ok(84)
let err = parse_config_int("not-a-number");
// err == Err("invalid digit found in string")
}
In parse_config_int, the ? after s.parse::() extracts 42 on success or returns Err("invalid digit found in string") on failure, because String has a From<ParseIntError> implementation. The second line does not need ? because value * 2 cannot fail.
Where `?` falls short
- Outside a Result‑returning function: Using
?in a function with a non‑Resultreturn type causes a compile error. The workaround is an explicitmatchor.map_err(). - Mismatched error types: If the error type carried by the expression cannot be converted into the function’s error type, the compiler emits
cannot convert error type <E> into <E>. This commonly happens when chaining?across functions with different error annotations without an explicitmap_errorFromimpl. - Closures and iterator adaptors: The
?operator does not apply directly in closures that do not returnResult. Use.ok_or_else(|| anyhow!("..."))or.map_err()to adapt. - Option without
From: Writingoption_value?in a-> Result<T, E>function requiresE: From<Option<E>>. If that impl is missing, the operator fails.
Verifying the behavior
To confirm the ? operator works as expected in your project:
- Create a new binary project:
cargo new --bin qdemo && cd qdemo - Replace
src/main.rswith a minimal test function that uses?on a known fallible operation, such asstr::parse. - Run:
cargo build - Expected check: the build succeeds and the function’s return type correctly propagates the error. If the error types are incompatible, cargo will halt with a clear "cannot convert error type" message.
- Optional verification: run
cargo check --edition 2021to ensure the behavior is consistent across editions, since the operator’s semantics are edition‑stable.
Relevant risk: If you add ? to a function whose return type does not declare Result, or whose error type lacks a From implementation for the expression’s error, the compiler will refuse to compile. The diagnostic message explicitly names the mismatched types, which is the practical way to identify and fix the mismatch.
Common pitfalls and how to avoid them
- Forgetting the Try bound. When defining your own error type, ensure it implements
std::error::Errorand provides aFromimpl for the types you want to propagate with?. Without it, the operator has no path to convert the error. - Over‑reliance on
?for non‑fallible expressions. Using?on a value that is alreadyOkor a plain integer introduces no benefit and can confuse readers. Reserve it for expressions whose type isResultorOptionwith a compatible error. - Ignoring error context. The
? operator strips the original error location. If you need richer diagnostics, consider usingeyreoranyhow, or manually add context withmap_errbefore the?.
The ? operator is one of Rust’s smallest syntactic additions with the largest impact on readability. By automating the “extract on success, forward on failure” pattern, it lets you write pipelines of fallible operations at the same verbosity as pure computations—provided the error‑type plumbing is in place.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.