RustArena

Step 4 of 7

The ? operator

Most functions that hit an error can't fix it themselves. A function that parses a number doesn't know whether the right reaction is to ask the user again, skip the line, or stop the program, so it hands the error to its caller. This is called propagating the error. With match, it looks like this:

fn add_quantities(a: &str, b: &str) -> Result<u32, ParseIntError> {
    let x: u32 = match a.trim().parse() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    let y: u32 = match b.trim().parse() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    Ok(x + y)
}

That pattern is so common that Rust has an operator for it. The ? operator, written after an expression that gives a Result:

use std::num::ParseIntError;

fn add_quantities(a: &str, b: &str) -> Result<u32, ParseIntError> {
    let x: u32 = a.trim().parse()?;
    let y: u32 = b.trim().parse()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", add_quantities("3", " 4"));
    println!("{:?}", add_quantities("3", "four"));
}
Ok(7)
Err(ParseIntError { kind: InvalidDigit })

Same behavior as the match version, in a third of the code. The happy path reads top to bottom, and every ? marks a place where the function can stop early. use std::num::ParseIntError; lets you write the error type by its short name.

? works anywhere in the function, including inside loops:

use std::num::ParseIntError;

// Sums a line of package weights like "12 7 30".
fn total_weight(line: &str) -> Result<u32, ParseIntError> {
    let mut total = 0;
    for word in line.split_whitespace() {
        total += word.parse::<u32>()?;
    }
    Ok(total)
}

fn main() {
    for line in ["12 7 30", "5 x 9", ""] {
        match total_weight(line) {
            Ok(kg) => println!("{line:?}: {kg} kg"),
            Err(e) => println!("{line:?}: {e}"),
        }
    }
}
"12 7 30": 49 kg
"5 x 9": invalid digit found in string
"": 0 kg

The first bad word stops the whole function. The caller (main) is where the decision gets made: here, print the problem and move on to the next line.

? on Option

In a function that returns Option, ? works on Option values: Some(v) gives v, None returns None from the function.

fn initials(full_name: &str) -> Option<String> {
    let mut words = full_name.split_whitespace();
    let first = words.next()?.chars().next()?;
    let last = words.next()?.chars().next()?;
    Some(format!("{first}{last}"))
}

fn main() {
    println!("{:?}", initials("Ada Lovelace"));
    println!("{:?}", initials("Ferris"));
}
Some("AL")
None

Common mistakes

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`)

The next step shows how main can return a Result.

fn parse_port(text: &str) -> Result<u16, String> {
    let port: u16 = text.parse()?;
    Ok(port)
}

fn main() {
    println!("{:?}", parse_port("80"));
}
error[E0277]: `?` couldn't convert the error to `String`
  |                    -------^ the trait `From<ParseIntError>` is not implemented for `String`

The fix is to make the types line up: return Result<u16, ParseIntError>, convert the error first (with map_err, two steps from now), or define your own error type that knows how to absorb a ParseIntError (next module).

Your task

A paint app reads colors as three numbers separated by spaces, like 255 128 0. Write parse_rgb using ? after each parse (no match, no unwrap() or expect()). The starter splits the line for you; unwrap_or("") gives an empty string for a missing part, which then fails to parse.

Challenge 2

A leaderboard shows the top three scores. Write fn podium(scores: &[u32]) -> Option<String> using ? on Option. The scores are already sorted, best first. With at least three scores, return Some("1st: 90, 2nd: 75, 3rd: 60") (with the real numbers); with fewer, return None.

scores.first() gives the first element as an Option<&u32>, and scores.get(1) the second.

Challenge 3

A receipt line lists items as <quantity>x<price in cents>, separated by spaces, like 3x250 1x1200 2x99. Write fn order_total(line: &str) -> Result<u32, ParseIntError> that returns the total in cents (3*250 + 1*1200 + 2*99 = 2148). An empty line totals 0. Any bad number (or a missing one, like 2x) must return the parse error, using ?.

Hint for splitting: item.split('x') gives the pieces around the x.

Previous: unwrap, expect and safe defaults Next: main can return a Result