The ? operator is a first-class citizen in Rust error handling. If Result is Err or Option is None, it immediately exits the entire function and returns that error to the caller. Deeply nested matchs turn into a single line, and when combined with the From trait, different error types can be smoothly grouped together. This is why almost every line in library code ends with ?.
Learning Objectives
- Understand the principle of how
?works (early return). - Use
?with bothResult<T, E>andOption<T>. - Observe how other error types are automatically converted thanks to the
Fromtrait. - Learn the pattern of setting the return type of
maintoResult<(), Box<dyn Error>>.
Key Concepts
1) What ? Does
let n: i32 = "42".parse()?;
The above single line is essentially as follows.
let n: i32 = match "42".parse() {
Ok(v) => v,
Err(e) => return Err(e.into()),
};
Core points:
Ok(v)→ The valuevremains in place.Err(e)→ The function itself immediately returns withErr(e.into()).
2) Where ? Applies
The return type of the function must be compatible with ?.
- Function returning
Result<T, E>:?can be used as aResult<_, _>value. - Function that returns
Option<T>:?can be used withOption<_>values.
If you want to mix Result and Option in the same function, unify to one side using ok_or / ok, or use the 16-pattern (common error type).
3) Automatic conversion of From
? is not simple propagation. If From<Original_Error> for Function_Error is implemented, it is automatically converted.
impl From<ParseIntError> for MyError { /* ... */ }
fn parse_age(s: &str) -> Result<u8, MyError> {
let n: i32 = s.parse()?; //ParseIntError → MyError automatic
/* ... */
}
Thanks to this, ? can neatly group multiple types of errors in one line.
4) Return type of main
main can also return Result.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let s = std::fs::read_to_string("data.txt")?;
let n: i32 = s.trim().parse()?;
println!("n = {n}");
Ok(())
}
Box<dyn Error> means "I will accept any error." Full-fledged libraries prefer 18 custom error types more.
Examples
Example 1 — ex01_read_file.rs: File + Parsing
//Example 16-1: Reading a file and parsing numbers — naturally propagate two types of errors with ?
//
//`?` immediately returns from the function if the Result/Option is Err/None and
//passes the error back to the caller. Both different error types are automatically converted to `Box<dyn Error>`
//and combined in one line.
use std::error::Error;
use std::fs;
fn read_first_number(path: &str) -> Result<i32, Box<dyn Error>> {
let content = fs::read_to_string(path)?; // io::Error
let n: i32 = content.trim().parse()?; // ParseIntError
Ok(n)
}
fn main() -> Result<(), Box<dyn Error>> {
//Create a file at a temporary path and read it.
let path = std::env::temp_dir().join("lecture16_ex01.txt");
fs::write(&path, "42")?;
let n = read_first_number(path.to_str().expect("UTF-8 path"))?;
println!("Read number: {n}");
Ok(())
}
Example 2 — ex02_option_q.rs: ? of Option
//Example 2 of Episode 16: ? also works with Option
//
//When a function's return type is Option<T>, ? propagates None directly to the caller.
fn last_char_uppercase(s: &str) -> Option<char> {
let last = s.chars().last()?; //If the string is empty, return None immediately
last.to_uppercase().next()
}
fn main() {
for s in ["hello", "rust", ""] {
println!("{:?} → {:?}", s, last_char_uppercase(s));
}
}
Example 3 — ex03_from.rs: From automatic conversion
//Example 3 of Chapter 16: `?` and automatic conversion with From
//
//The `?` does not simply return the error as is; if the From trait is implemented,
//it **automatically converts** to the caller's error type.
use std::num::ParseIntError;
#[derive(Debug)]
enum MyError {
Parse(ParseIntError),
OutOfRange(i32),
}
//Implementing From to wrap standard library errors into our enum
impl From<ParseIntError> for MyError {
fn from(e: ParseIntError) -> Self {
MyError::Parse(e)
}
}
fn parse_age(s: &str) -> Result<u8, MyError> {
let n: i32 = s.parse()?; //ParseIntError → automatic conversion to MyError
if !(0..=150).contains(&n) {
return Err(MyError::OutOfRange(n));
}
Ok(n as u8)
}
fn main() {
for s in ["25", "0", "abc", "200", "-3"] {
println!("{s:?} → {:?}", parse_age(s));
}
}
Common Mistakes
Q. I use ? in main but get the error "the ? operator can only be used..."
A. It is because the return type of fn main() is (). Change it to fn main() -> Result<(), Box<dyn Error>>.
Q. I want to propagate Option::None as ? inside a Result function.
A. It cannot be used as-is. First, convert ok_or(...) to Result.
let v = opt.ok_or("No value")?;
Q. Do I need to implement From directly?
A. Many Froms are already implemented among standard errors, and if you receive it as Box<dyn Error>, it is almost automatic. When creating your own error type, you need impl From<...> directly, but it can be automated with the thiserror macro in chapter 18.
Q. Can I always use ? in place of unwrap?
A. ? only makes sense when the caller is prepared to receive errors. For small script mains, expect is sufficient — just be careful with the messages shown to the user.
Summary
?terminates the function early when Err/None, propagating the error.- Works with both
ResultandOption— but the function return type must be compatible. - Multiple error types can be naturally combined with
Fromtrait auto-conversion. maincan returnResult<(), Box<dyn Error>>as well.
Try it yourself
Solve the assignment below. The reference solution is shown below.