Why this matters
Most functions that can fail do so because something they call failed: reading a config fails because opening the file failed, parsing a record fails because one field did not parse. The usual response is to propagate the error to the caller, who knows better what to do. Writing a match for every call buries the actual logic under error plumbing. The ? operator does the plumbing in one character, which is why idiomatic Rust error handling is both explicit and concise.
Propagating by hand
Here is a function that reads two numbers from text and adds them, with errors propagated manually:
use std::num::ParseIntError;
fn add_strs(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = match a.trim().parse::<i32>() {
Ok(v) => v,
Err(e) => return Err(e),
};
let y = match b.trim().parse::<i32>() {
Ok(v) => v,
Err(e) => return Err(e),
};
Ok(x + y)
}
fn main() {
println!("{:?}", add_strs("2", " 40"));
println!("{:?}", add_strs("2", "forty"));
}
Ok(42)
Err(ParseIntError { kind: InvalidDigit })
Every fallible call follows the same shape: "if Ok, give me the value; if Err, return it".
The ? operator
? is exactly that shape:
use std::num::ParseIntError;
fn add_strs(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = a.trim().parse::<i32>()?;
let y = b.trim().parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strs("2", " 40"));
println!("{:?}", add_strs("2", "forty"));
}
Ok(42)
Err(ParseIntError { kind: InvalidDigit })
expr? on a Result:
Ok(v): the whole expression evaluates tov, and execution continues.Err(e): the function returns immediately withErr(From::from(e)).
It works on Option too (None returns None), as you saw in lesson 1. It can be chained: s.split_once(',').ok_or("no comma")?.0.trim().parse::<i32>()? reads left to right as a sequence of steps, any of which may bail out.
? only works in functions that return Result (or Option)
? needs somewhere to return the error to:
fn main() {
let n: i32 = "5".parse()?;
println!("{n}");
}
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> main.rs:2:29
|
1 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
2 | let n: i32 = "5".parse()?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, _>>` is not implemented for `()`
help: consider adding return type
|
1 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
2 | let n: i32 = "5".parse()?;
3 | println!("{n}");
4 + Ok(())
|
E0277: main returns (), so there is no error to return. The fix the compiler suggests is the next topic.
main can return Result
main may return Result<(), E> for any error type that can be printed with Debug. If it returns Err, Rust prints Error: followed by the Debug form of the error to stderr and exits with status 1:
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let n: i32 = "5".parse()?;
println!("parsed {n}");
let m: i32 = "five".parse()?;
println!("never printed {m}");
Ok(())
}
parsed 5
Error: ParseIntError { kind: InvalidDigit }
Box<dyn Error> means "any error type at all, stored on the heap". dyn Error is a trait object (module 10); Box is a heap pointer (module 13). For now, Result<(), Box<dyn Error>> is the standard "anything can go wrong here" return type for main and for small programs: ? converts every standard error type into it automatically. At the end of a successful main, return Ok(()) (() is the unit value: "nothing interesting").
The printed message uses Debug formatting, which is fine for tools you use yourself. For polished user-facing messages, match the error in main, print it nicely with eprintln! and call std::process::exit.
? converts errors with From
When a function calls operations with different error types, it needs one error type for its own signature. ? helps: before returning an error, it calls From::from on it, so any error type that can be converted into the function's error type works. Box<dyn Error> can be created from every standard error, which is why it is so convenient:
use std::error::Error;
use std::fs;
fn read_number(path: &str) -> Result<i64, Box<dyn Error>> {
let text = fs::read_to_string(path)?;
let n = text.trim().parse::<i64>()?;
Ok(n * 2)
}
fn main() {
match read_number("/no/such/file") {
Ok(n) => println!("{n}"),
Err(e) => println!("failed: {e}"),
}
match read_number("/proc/sys/kernel/pid_max") {
Ok(n) => println!("twice pid_max: {n}"),
Err(e) => println!("failed: {e}"),
}
}
failed: No such file or directory (os error 2)
twice pid_max: 8388608
fs::read_to_string fails with an io::Error, parse with a ParseIntError; both convert to Box<dyn Error>. Printing the boxed error with {} shows the underlying error's message. (The second call reads a Linux kernel setting exposed as a file; the value depends on the machine.) Lesson 4 builds a custom error type with its own From conversions, which is what libraries do.
Turning Option into Result for ?
In a function returning Result, you cannot ? an Option directly (there is no error value). Convert it with ok_or (or ok_or_else, which builds the error lazily):
fn first_word_len(s: &str) -> Result<usize, String> {
let w = s.split_whitespace().next().ok_or("empty input")?;
Ok(w.len())
}
fn main() {
println!("{:?}", first_word_len("hello world"));
println!("{:?}", first_word_len(" "));
}
Ok(5)
Err("empty input")
"empty input" is a &str; ? converts it to the String error type via From<&str> for String.
Adding context with map_err
Raw errors often lack context ("invalid digit found in string": which string?). map_err replaces or wraps an error before ? propagates it:
fn parse_field(name: &str, value: &str) -> Result<u32, String> {
let n = value.trim().parse::<u32>().map_err(|e| format!("field {name}: {value:?}: {e}"))?;
Ok(n)
}
fn main() {
println!("{:?}", parse_field("age", "41"));
println!("{:?}", parse_field("age", "4l"));
}
Ok(41)
Err("field age: \"4l\": invalid digit found in string")
Common mistakes
?in a function returning(): E0277. Change the return type toResult<..., ...>(and returnOk(...)at the end).- Forgetting the final
Ok(()): E0308, expectedResult<(), _>, found(). - Mismatched error types without a conversion:
?on anio::Errorin a function returningResult<_, String>is E0277 "?couldn't convert the error toString". Usemap_err(|e| e.to_string())?, aBox<dyn Error>return type, or a custom error type withFrom(lesson 4). - Using
?where you should handle the error: propagate when the caller can decide better; handle locally when you know the right recovery (default value, retry, skip the line).
Recap
expr?evaluates to the success value, or returns early from the function with the error (orNone).- Before returning,
?converts the error withFrom::frominto the function's error type. ?works only in functions returningResultorOption;maincan returnResult<(), Box<dyn Error>>.ok_orturns anOptioninto aResult;map_erradds context or converts errors.Box<dyn Error>accepts any standard error and is the easy choice for applications.
Going further: read the "Recoverable Errors with Result" chapter of the Rust Book (9.2), in particular the section "A Shortcut for Propagating Errors: the ? Operator".