Vibe Engines
YouTube

Lesson 7.2 · 9 min

The ? operator

Chatterbox now has three things that can go wrong on the way from "/last 3" to a list of messages. Writing a match for each one would bury the real logic. One character fixes that.

Adds to ChatterboxErrors pass upward in one character.

Passing the problem upward

A function that calls another function that can fail has a choice. It can handle the failure right there with match, or it can say "I cannot deal with this, my caller will" and return the Err immediately. The second choice is so common that Rust gives it one character: ?.

Put ? after something that returns a Result. If it is Ok(v), the whole expression becomes v and the function carries on. If it is Err(e), the function stops right there and returns that Err to its caller.

Three lines of match become one character
use std::num::ParseIntError;
 
fn double(arg: &str) -> Result<u32, ParseIntError> {
let n = arg.trim().parse::<u32>()?;
Ok(n * 2)
}
 
fn main() {
println!("{:?}", double("21"));
println!("{:?}", double("x"));
}
$ cargo run
Ok(42)
Err(ParseIntError { kind: InvalidDigit })

Two rules come with it. ? only works inside a function that itself returns a Result, because it needs somewhere to send the Err. And the success value gets wrapped again at the end with Ok(...), since the function promises a Result.

An error type of our own

Chatterbox has more than one way to fail: the count is not a number, it is zero, or it asks for more messages than exist. A single ParseIntError cannot say all of that. So we make a small enum, one variant per kind of failure, and teach it to print a sentence a person can read.

A variant can also carry named fields in braces, as TooMany { asked: u32, have: usize } does below; you build it with the same braces and take it apart with the same braces in a pattern.

Printing with {} needs the type to implement fmt::Display. (That word implement belongs to traits: a trait is a named promise that a type can do something, and chapter 8 tells the full story. For now, copy this shape and change only the sentences.) The shape is always the same: a method fmt that writes text into the formatter f using write!, which works like println! but sends the text to f.

ChatError, and the ? that feeds it
use std::fmt;
use std::num::ParseIntError;
 
#[derive(Debug)]
enum ChatError {
BadCount(ParseIntError),
Zero,
TooMany { asked: u32, have: usize },
}
 
impl fmt::Display for ChatError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ChatError::BadCount(e) => write!(f, "that is not a number ({e})"),
ChatError::Zero => write!(f, "ask for at least 1 message"),
ChatError::TooMany { asked, have } => {
write!(f, "you asked for {asked} but there are only {have}")
}
}
}
}
 
fn parse_last(arg: &str, have: usize) -> Result<usize, ChatError> {
let n = arg.trim().parse::<u32>().map_err(ChatError::BadCount)?;
if n == 0 {
return Err(ChatError::Zero);
}
if n as usize > have {
return Err(ChatError::TooMany { asked: n, have });
}
Ok(n as usize)
}
 
fn main() {
for arg in ["2", "two", "0", "9"] {
match parse_last(arg, 3) {
Ok(n) => println!("/last {arg}: ok, {n} messages"),
Err(e) => println!("/last {arg}: {e}"),
}
}
}
$ cargo run
/last 2: ok, 2 messages
/last two: that is not a number (invalid digit found in string)
/last 0: ask for at least 1 message
/last 9: you asked for 9 but there are only 3

map_err changes the reason inside an Err and leaves an Ok alone. Here it wraps the ParseIntError in our BadCount variant, so every failure in the function is a ChatError and ? has one type to return. (Rust can do that wrapping for you with a trait called From; that comes in chapter 8, and map_err shows the same idea by hand.)

PredictWhich message prints?
use std::num::ParseIntError;
 
fn first(a: &str, b: &str) -> Result<u32, ParseIntError> {
let x = a.parse::<u32>()?;
let y = b.parse::<u32>()?;
Ok(x + y)
}
 
fn main() {
match first("4", "oops") {
Ok(n) => println!("{n}"),
Err(_) => println!("failed"),
}
}

What does this print?

  • 4
  • failed
  • 0
Reveal the answer

The program prints: failed The first ? succeeds and gives 4. The second sees an Err and returns it from first at once, so the sum never happens and main takes the Err arm.

main can return a Result too

Because main is a function, it may return Result<(), SomeError>. Then ? works inside it, and if an Err reaches the top the program exits with an error message. You will use this when the program starts touching files, a preview of the last lesson in this chapter.

Try itYour turn: validate /last

Finish parse_limited, a stricter cousin of the listing above. Parse the argument with ?, then return Zero for 0, OverLimit when the count is above max (checked first), TooMany when more is asked than have, and Ok(n) otherwise. Asking for exactly max or exactly have is fine.

Finish parse_limited, a stricter cousin of the listing above. Parse the argument with ?, then return Zero for 0, OverLimit when the count is above max (checked first), TooMany when more is asked than have, and Ok(n) otherwise. Asking for exactly max or exactly have is fine.

use std::num::ParseIntError;
 
#[derive(Debug, PartialEq)]
enum ChatError {
BadCount(ParseIntError),
Zero,
OverLimit { max: u32 },
TooMany { asked: u32, have: usize },
}
 
fn parse_limited(arg: &str, have: usize, max: u32) -> Result<usize, ChatError> {
// parse with ?, then check zero, the limit, and too many
Ok(0)
}
 
fn main() {
println!("{:?}", parse_limited("2", 5, 4));
println!("{:?}", parse_limited("9", 5, 4));
}

Hidden tests (7): over_the_limit_is_refused_first text_is_a_bad_count exactly_all_is_fine good_count exactly_max_is_fine too_many_is_refused zero_is_refused

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

What you have now

Chatterbox can name what went wrong in a sentence. Errors travel up with ? and are explained once, at the top. As a preview, the next two lessons tidy the growing file and then let the history live on disk.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What does ? do on an Err?Reveal
It returns that Err from the current function immediately. On an Ok it unwraps the value and continues.
Where can you use ?Reveal
Only inside a function that returns a Result (or Option), because it needs somewhere to send the failure.
Why make an enum for errors?Reveal
One variant per kind of failure lets the caller match on the cause and lets Display say it in a sentence.

That is lesson 7.2. Tick it off so the course remembers where you are.

Learn Rust by building a chat app · Lesson 7.2 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026