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:
- on
Ok(v): unwraps it, so the expression is justv - on
Err(e): returnsErr(e)from the current function immediately
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
- Using
?in a function that returns().?returns early with the error, so the function must return aResult(or anOption). That includesmain:
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.
- Mismatched error types.
?can only pass the error on if it fits the function's error type. Here the function promises aStringerror butparseproduces aParseIntError:
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).
- Using
?on anOptionin a function that returnsResult(or the other way around). They don't mix: you getthe `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`(or the same message with the two swapped). Turning one into the other is also coming up with the combinators.
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