14 / 20 · Day 5
Day 5 · Concept 14

The ? operator

One character that propagates Err or None up the call stack. Replaces the entire "if err != nil { return nil, err }" pattern from Go. Works on Result and Option.


1 · The pattern it replaces

rust src/main.rs · without ?
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
    let n = match s.parse::<i32>() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    Ok(n * 2)
}
rust src/main.rs · with ?
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
    let n = s.parse::<i32>()?;   // unwrap-or-return
    Ok(n * 2)
}

fn main() {
    println!("{:?}", parse_and_double("21"));
    println!("{:?}", parse_and_double("nope"));
}

2 · Chain it

rust src/main.rs · multiple ?s in a row
use std::fs;
use std::io;

fn read_first_line(path: &str) -> io::Result<String> {
    let contents = fs::read_to_string(path)?;        // io::Error propagates
    let first = contents.lines().next().unwrap_or("").to_string();
    Ok(first)
}

Each ? says "if Err, return early; if Ok, unwrap and keep going". The function signature spells out the error type once.

3 · Error type conversion

? also calls From::from on the error, so it converts between error types automatically — as long as a From impl exists. The Box-based catchall trick:

rust src/main.rs · Box<dyn Error> swallows anything
fn run() -> Result<(), Box<dyn std::error::Error>> {
    let n: i32 = "42".parse()?;           // ParseIntError -> Box<dyn Error>
    let _ = std::fs::read_to_string("x")?; // io::Error -> Box<dyn Error>
    println!("{}", n);
    Ok(())
}
Prototyping vs production. Box<dyn Error> in main() or scripts is fine. In libraries, define a real error enum so callers can match on variants. The next concept covers that.

4 · ? on Option

rust src/main.rs
fn first_char_upper(s: &str) -> Option<char> {
    let c = s.chars().next()?;           // None -> early return
    c.to_uppercase().next()
}

fn main() {
    println!("{:?}", first_char_upper("hello"));
    println!("{:?}", first_char_upper(""));
}

Same semantics: None in, None out. Some(v) unwraps and continues.

5 · Common mistakes

  • Using ? in a function that returns (). Doesn't compile. The enclosing function must return Result or Option.
  • Mixing Result and Option without a From bridge. Use .ok_or(err) to convert OptionResult before ?.
  • main() returning (). To use ? in main, declare fn main() -> Result<(), Box<dyn Error>>.

6 · When it clicks

  • You write linear-looking code with ? at every fallible call.
  • You stop reaching for .unwrap() outside tests and prototypes.
  • You read ? as "unwrap or propagate" without thinking.
Found this useful?