← Fundamentals
Guided lesson 7 questions · Rust only

Rust Result, ?, and panic

Follow what ? returns and converts, where it is allowed, and when a panic is the right answer.

In Rust, ? is a short match: it gives the value inside Ok or Some, or returns the failure from the current function, converting an error with From. Return an Err for a failure the calling code can cause; panic, with unwrap or expect, only when a failure would mean a bug in your own code.

Written for Rust.

Before you begin

Why this matters

Most Rust functions that can fail have lines that end in ?, and many crashes in Rust programs come from an unwrap or an expect. If you can say what each ? returns and converts, and what each unwrap claims, you can read every way a function can fail from its code.

What this helps you reason about

  • Say what a ? returns, and which From impl converts the error.
  • Fix the compiler’s E0277 errors for ? in main, on an Option, and inside a closure.
  • Decide between returning Err and panicking with expect, and write an expect message that helps.

In Java or Python, a failure jumps out of a function as an exception, and a try block catches it. In Rust, a failure that the calling code should expect is a returned value of type Result, and ? passes it on in one character. Errors as values or exceptions and Propagation and unwinding showed match, ?, and a first From conversion. This lesson goes further at a school science fair, where judges type their scores as text: what ? writes for you, where it is allowed, which small methods replace a match, and when a panic is the right answer.

Already know this? The short version
  • x? on a Result gives the value inside Ok, or returns Err(From::from(e)) from the current function; the From impl is chosen by the type of the error, so an error enum with one From impl for each source type lets ? convert each.
  • ? works on a Result in a function that returns Result, and on an Option in a function that returns Option (E0277 otherwise); turn an Option into a Result with ok_or, and let main return Result<(), E> to use ? there: an Err prints Error: and its debug form, and the program exits with status 1.
  • map_err, ok_or, unwrap_or, and and_then replace short matches; none of them returns from the function, so follow them with ?.
  • unwrap and expect claim that a failure is a bug: use them for values the program made or checked, and return Err for input. The section “What goes wrong with Result in real programs?” covers arguments to unwrap_or that always run, ? inside a closure, and the debug text that main prints.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
Result
A type whose value is either Ok(x) for success or Err(e) for failure. Functions that can fail, such as parse, return a Result.
? operator
An operator written after a call that returns a Result. It gives the value inside Ok, or returns the Err from the current function at once.
propagate
To pass a failure on to your own caller instead of handling it. An exception propagates by itself; a failure returned as a value propagates only when each caller returns it again.
From
A standard trait for converting one type into another. impl From<A> for B lets B::from(a) build a B from an A, and lets ? convert an error of type A into B.
Option
A type whose value is either Some(x), holding a value, or None, holding nothing. Option<u32>, for example, is a number that may be missing.
map_err
A Result method that changes the error inside an Err with a function, and leaves an Ok unchanged.
panic
Rust’s way of stopping because of a bug: the current thread stops with a message such as thread 'main' panicked at …. In a program with one thread, the program stops.
expect
A method like unwrap that also takes a message. It gives the value inside Ok or Some, and panics with the message when there is none; for an Err, the panic message also shows the error.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Matching errors

How do you handle each kind of failure?

Result<T, E> is an enum with two variants, Ok(T) and Err(E), and a match handles both, as the earlier lessons showed. When E is an enum of your own, one match can also tell the failures apart. The pattern Err(ScoreError::TooHigh(p)) matches only an Err whose error is TooHigh, and gives the number inside it the name p. _ matches any value without naming it.

The compiler checks that the arms cover every combination of Ok, Err, and each error variant. Leave one out, and it reports error[E0004]: non-exhaustive patterns: `Err(NotANumber(_))` not covered. So when someone adds a variant to the error enum, every match that must handle it stops compiling until it does.

Inside score, the match on the Result from parse returns early on Err. That is the long form of ?, which the next section shortens.

main.rs
enum ScoreError {
    NotANumber(ParseIntError),
    TooHigh(u32),
}

match score(typed) {
    Ok(p) => println!("{} points", p),
    Err(ScoreError::TooHigh(p)) => println!("{} is too high", p),
    Err(ScoreError::NotANumber(_)) => println!("not a number"),
}

With an error enum, one match handles success and each kind of failure by name, and the compiler checks that none is missing.

Coming from Java: Each Err(…) arm does the job of one catch block, but the compiler makes you handle every variant, and the order of the arms does not follow a class hierarchy.

Precisely: Result is an ordinary enum

The standard library defines enum Result<T, E> { Ok(T), Err(E) }, with nothing built into the language except ?. The type is marked #[must_use], which is why the compiler warns about a Result that nothing uses; the stepper does not show Rust warnings. ParseIntError, the error type of parse for whole numbers, lives in std::num.

Experiment

Match each kind of failure by name

A pattern can look inside two enums at once: Err(ScoreError::TooHigh(p)) matches only an Err whose error is TooHigh, and names the number inside p. The arms are tried from the top. Result and ScoreError each have a fixed list of variants, so the compiler can check that the arms cover every combination.

Why it matters Give your errors an enum when the calling code must react differently to each, and let match check that every kind is handled.

Predict the three output lines, and which arm of the match in main each score reaches. Then delete the line Err(ScoreError::NotANumber(_)) => println!("not a number"), and predict what the compiler says.

use std::num::ParseIntError;

#[derive(Debug)]
enum ScoreError {
    NotANumber(ParseIntError),
    TooHigh(u32),
}

fn score(typed: &str) -> Result<u32, ScoreError> {
    let points = match typed.parse::<u32>() {
        Ok(points) => points,
        Err(problem) => return Err(ScoreError::NotANumber(problem)),
    };
    if points > 10 {
        return Err(ScoreError::TooHigh(points));
    }
    Ok(points)
}

fn main() {
    for typed in ["7", "12", "seven"] {
        let result = score(typed);
        match result {
            Ok(p) => println!("{} points", p),
            Err(ScoreError::TooHigh(p)) => println!("{} is too high", p),
            Err(ScoreError::NotANumber(_)) => println!("not a number"),
        }
    }
}
What to look for

Step through main. After each call, result shows the Result. For "7" it is Ok(7). For "12" it is Err(TooHigh(12)), and for "seven" it is Err(NotANumber(…)) with a ParseIntError inside, whose kind is InvalidDigit. The stop after match result is the arm that matched: line 24, then line 25, then line 26. When score returns an Err, it also stops once at line 1, which is code the compiler wrote for the return, not a line of yours. The run prints 7 points, 12 is too high, not a number. After the edit, nothing runs, and the compiler reports error[E0004]: non-exhaustive patterns: `Err(NotANumber(_))` not covered.

Expected output

7 points
12 is too high
not a number

02 / What ? does

What does ? write for you?

let points = score.parse::<u32>()?; does the work of a match. For Ok(v), ? gives v. For Err(e), it returns Err(From::from(e)) from the current function. So ? does two things: it returns early, to propagate the failure, and it converts the error into the error type of the function it is written in.

The conversion comes from an impl From<X> for FairError block, which says how to build a FairError from an X. The compiler chooses the impl by the type of the error: ParseIntError from a whole-number parse uses one impl, ParseFloatError from a decimal parse uses another. When the error already has the function’s error type, no impl is needed, because the standard library has impl From<T> for T.

Without a matching impl, the compiler reports E0277, as Propagation and unwinding showed.

main.rs
// What ? does, written out:
let points = match score.parse::<u32>() {
    Ok(points) => points,
    Err(problem) => return Err(From::from(problem)),
};

impl From<ParseIntError> for FairError {
    fn from(problem: ParseIntError) -> FairError {
        FairError::BadScore(problem)
    }
}

? returns Err(From::from(e)): an early return, plus a conversion chosen by the type of the error.

Coming from Go: ? is if err != nil { return …, err } with a type conversion added, and you never write the return line yourself.

Precisely: FromResidual and other conversions

The exact definition of ? uses the traits Try and FromResidual, which are not yet stable, so compiler messages mention FromResidual. Through From, ? can also turn a &str error into a String (impl From<&str> for String), and any error that implements std::error::Error into Box<dyn Error>. A Box<dyn Error> accepts every error, but the calling code can no longer match on which one it was.

Experiment

Watch ? choose a From impl

? on an Err calls From::from with the error. The compiler picks the impl whose source type is the type of that error: a ParseIntError goes to the first impl, a ParseFloatError to the second. Then ? returns the converted Err from entry, so the lines after it do not run.

Why it matters With one From impl for each error type, ? can pass different failures on through one error enum. The type of the error alone decides which impl runs.

The second call fails on the distance. Predict which from function the stepper enters, and what the second line prints. Then change entry("8", "far") to entry("eight", "far") and predict again.

use std::num::{ParseFloatError, ParseIntError};

#[derive(Debug)]
enum FairError {
    BadScore(ParseIntError),
    BadDistance(ParseFloatError),
}

impl From<ParseIntError> for FairError {
    fn from(problem: ParseIntError) -> FairError {
        FairError::BadScore(problem)
    }
}

impl From<ParseFloatError> for FairError {
    fn from(problem: ParseFloatError) -> FairError {
        FairError::BadDistance(problem)
    }
}

fn entry(score: &str, metres: &str) -> Result<String, FairError> {
    let points = score.parse::<u32>()?;
    let distance = metres.parse::<f64>()?;
    Ok(format!("{} points, {} m", points, distance))
}

fn main() {
    println!("{:?}", entry("8", "3.5"));
    println!("{:?}", entry("8", "far"));
}
What to look for

In the first call, both ? find an Ok, the stepper never leaves entry, and the run prints Ok("8 points, 3.5 m"). In the second call, points shows 8, and from line 23 the stepper enters main::{impl#1}::from. The stepper names a method by the position of its impl block, counted from 0, so {impl#1} is the second block, From<ParseFloatError>. Its problem shows {kind: …FloatErrorKind::Invalid}. Line 24 never runs, and the run prints Err(BadDistance(ParseFloatError { kind: Invalid })). After the edit, the stepper goes from line 22 into main::{impl#0}::from, line 23 never runs, and the second line is Err(BadScore(ParseIntError { kind: InvalidDigit })).

Expected output

Ok("8 points, 3.5 m")
Err(BadDistance(ParseFloatError { kind: Invalid }))

03 / Where ? works

Where can ? be used?

? returns from the function it is written in, so that function’s return type must be able to hold what ? returns. In a function that returns Result, ? works on a Result. In a function that returns Option, ? works on an Option: Some(v) gives v, and None returns None at once.

Mixing them does not compile. ? on an Option inside a function that returns Result gives error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`. The help says what to do: use `.ok_or(...)?`. ok_or turns None into an Err that holds the error you give it, and Some(v) into Ok(v).

main returns nothing by default, so ? in main gives error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`). main may return Result<(), E> instead, and end with Ok(()). When main returns an Err, the program writes Error: and the debug form of the error to standard error, here Error: ParseIntError { kind: InvalidDigit }, and exits with status 1.

main.rs
fn neighbours(tables: &[&str], at: usize) -> Option<String> {
    let left = tables.get(at)?; // None ends the call here
    let right = tables.get(at + 1)?;
    Some(format!("{} next to {}", left, right))
}

fn main() -> Result<(), std::num::ParseIntError> {
    let points = "8".parse::<u32>()?;
    println!("{} points", points);
    Ok(())
}

? works on a Result in a function that returns Result, and on an Option in a function that returns Option; ok_or turns an Option into a Result.

Coming from TypeScript: tables[5]?.length gives undefined and the function keeps going; tables.get(5)? leaves the whole function with None.

Precisely: what main may return

main may return any type that implements the trait std::process::Termination. For Result<(), E>, the error type E must implement Debug, because that is how the error is printed. The stepper’s output panel shows only standard output; when a run ends with an error, the closing message shows the exit status and what was written to standard error.

Experiment

Leave a function early with ? on an Option

? on None returns None from neighbours, the way ? on an Err returns the Err. A function that returns Result has no None to return, and ? does not make up an error, so the compiler asks you to choose one with ok_or.

Why it matters Use ? on an Option inside a function that returns Option. Inside a function that returns Result, say which error a None means: .ok_or(…)?.

Predict both output lines, and which lines of neighbours run when at is 2. Then change -> Option<String> to -> Result<String, String> and Some(format!(…)) to Ok(format!(…)), and predict what the compiler says.

fn neighbours(tables: &[&str], at: usize) -> Option<String> {
    let left = tables.get(at)?;
    let right = tables.get(at + 1)?;
    Some(format!("{} next to {}", left, right))
}

fn main() {
    let tables = ["Volcano", "Magnets", "Bridges"];
    println!("{:?}", neighbours(&tables, 1));
    println!("{:?}", neighbours(&tables, 2));
}
What to look for

When at is 1, left and right each show an address, then → and a project name: get gives a reference into tables. The run prints Some("Magnets next to Bridges"). When at is 2, left shows Bridges, tables.get(3) gives None, and the stepper never reaches line 4: the next stop is the closing brace, and the second line is None. After the edit, the compiler refuses both ? lines with error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`, and the help use `.ok_or(...)?` to provide an error compatible with `Result<String, String>`.

Expected output

Some("Magnets next to Bridges")
None

04 / Small methods

Which methods replace a short match?

Some matches are so common that Result and Option have methods for them. Each method takes the Result or Option and gives a new value. None of them returns from the function, so they are often followed by ?.

  • map_err(f) changes the error inside an Err with the function f; an Ok passes through.
  • ok_or(e) turns Some(v) into Ok(v), and None into Err(e).
  • unwrap_or(d) gives the value inside Ok or Some, or d when there is none.
  • and_then(f) passes the value inside Ok or Some to f, a step that can fail itself, and gives the result of f; an Err or a None passes through.

Use unwrap_or only when a failure really means the default. Absence, zero, and empty values showed how unwrap_or(0) makes a missing value look like a real 0.

main.rs
let tables = ["Volcano", "Magnets"];
// Err("table 5 is empty"), not None
let fifth = tables.get(5).ok_or("table 5 is empty");
// Err("above 10"): parse, then check the range
let points = "12"
    .parse::<u32>()
    .map_err(|_| "not a number")
    .and_then(|p| if p <= 10 { Ok(p) } else { Err("above 10") });
// 0, because "eight" is not a number
let zero = "eight".parse::<u32>().unwrap_or(0);

map_err, ok_or, unwrap_or, and and_then each replace a short match; follow them with ? to pass the failure on.

Precisely: map and and_then

map(f) changes the value inside Ok or Some and cannot fail. If f itself returns a Result, map gives a Result inside a Result, Ok(Err(…)); and_then gives the inner Result instead. ok() and err() turn a Result into an Option, and transpose swaps an Option of a Result into a Result of an Option.

05 / Panic or Err

When should a program panic instead?

unwrap and expect turn an Err or a None into a panic. expect also takes a message, and the panic shows that message, a colon, and the error, then the file, line, and column: thread 'main' panicked at 'judges type whole numbers: ParseIntError { kind: InvalidDigit }', main.rs:6:14.

Read every unwrap and expect as a claim: this cannot fail unless the program has a bug. The claim is true for a value that the program made itself or has already checked, such as a number written in the code. It is false for text that a person typed, or anything read from a file or another program: there, a failure is normal, and the function should return Err. Write the expect message as the reason why the claim holds, so that a panic says which assumption broke.

So choose by who can cause the failure. A failure that the calling code can cause, and can do something about, is an Err. A broken assumption inside your own code is a bug, and a panic stops the program before it goes on with wrong data.

main.rs
// A claim about the program's own value: true.
let max: u32 = "10".parse().expect("10 is a whole number");
// A claim about typed text: false, so return Err instead.
let points: u32 = typed.parse().expect("judges type whole numbers");

unwrap and expect claim that a failure is a bug: use them for the program’s own values, and return Err for anything from outside.

Coming from Python: expect is closer to assert than to raise: it is for things that should never happen, not for bad input.

Precisely: the panic message and exit status

rustc 1.63, the stepper’s compiler, prints the location and the message on one line, as above; newer versions print the location first and the message on the next line. A panic in the main thread unwinds it and ends the program with exit status 101. panic!, unreachable!, and assert! panic on purpose. A build with panic = "abort" stops at once without unwinding.

Experiment

Turn a typed score into a panic with expect

expect gave the number for each Ok. For the Err it panicked: the main thread unwound, and the program stopped with exit status 101 before the println! line. The message claims that judges type whole numbers, and the input proved the claim false.

Why it matters Text that a person types can fail to parse, so that failure is not a bug. Return a Result from total and use ?; keep expect for values the program made or already checked.

Predict the output. Then change "9" to "nine" and predict what happens.

fn total(scores: &[&str]) -> u32 {
    let mut sum = 0;
    for typed in scores {
        let points: u32 = typed
            .parse()
            .expect("judges type whole numbers");
        sum += points;
    }
    sum
}

fn main() {
    println!("total {}", total(&["7", "9", "8"]));
}
What to look for

Step through the loop. typed shows an address, then → and the score text, because the loop gives a reference to each &str. points and sum change on each pass, and the run prints total 24. After the edit, the last stop is line 4 with typed → "nine", and the run ends there with nothing printed: thread 'main' panicked at 'judges type whole numbers: ParseIntError { kind: InvalidDigit }', main.rs:6:14, the line and column of expect. The message is the expect text, a colon, and the error’s debug form. The 7 already added to sum is lost too.

Expected output

total 24

06 / In production

What goes wrong with Result in real programs?

Programs that compile can still handle failures badly. Three mistakes are common.

The fallback runs every time. unwrap_or(ask_again(4)) calls ask_again before unwrap_or starts, even when parse succeeded, because every argument is computed before the call, as Evaluation order explains. A fallback with a side effect then prints, counts, or changes something on every call; one without a side effect wastes the work, such as building a message with format! that nobody reads. ok_or and expect(&format!(…)) behave the same. unwrap_or_else and ok_or_else take a closure, which runs only for an Err or a None.

? inside a closure returns from the closure, not from the function around it. .map(|t| t.parse::<u32>()?) gives error[E0277]: the `?` operator can only be used in a closure that returns `Result` or `Option` (or another type that implements `FromResidual`). Let the closure return the Result and collect into a Result, as Rust iterators and adapters shows, or use a for loop, where ? leaves the function.

main’s Err prints debug text. Error: ParseIntError { kind: InvalidDigit } is fine for a tool you run yourself. For a message that people read, match the Err in main and print a sentence.

main.rs
fn ask_again(table: u32) -> u32 {
    println!("ask the judge at table {} again", table);
    0
}

// ask_again runs although "9" is a number.
let score = "9".parse::<u32>().unwrap_or(ask_again(4));
// ask_again runs only for an Err.
let score = "9".parse::<u32>().unwrap_or_else(|_| ask_again(4));

A fallback passed to unwrap_or always runs; ? in a closure leaves only the closure; main’s Err prints debug text.

Experiment

Call a fallback that was never needed

unwrap_or(ask_again(4)) is a call with an argument, and Rust computes every argument before the call starts. So ask_again runs first, whatever parse gave. unwrap_or_else takes a closure instead, and calls it only for an Err; |_| ignores the error that the closure is given.

Why it matters When the fallback does work, such as printing, counting, or building a message, pass a closure to unwrap_or_else or ok_or_else.

parse succeeds here. Predict whether ask_again prints its line. Then change unwrap_or(ask_again(4)) to unwrap_or_else(|_| ask_again(4)) and predict again.

fn ask_again(table: u32) -> u32 {
    println!("ask the judge at table {} again", table);
    0
}

fn main() {
    let typed = "9";
    let score = typed.parse::<u32>().unwrap_or(ask_again(4));
    println!("table 4: {} points", score);
}
What to look for

From line 8, the stepper enters ask_again with table 4 before score has a value, and the output shows ask the judge at table 4 again. Then score shows 9, and the run prints table 4: 9 points. After the edit, the stepper goes from line 8 straight to line 9: ask_again never runs, and the only line printed is table 4: 9 points.

Expected output

ask the judge at table 4 again
table 4: 9 points

07 / Try it

Can you predict which error each call returns?

Each call prints the table first. Decide which ? returns, and which error variant main prints.

main.rs
use std::num::ParseIntError;

#[derive(Debug)]
enum PrizeError {
    NoTable(usize),
    BadScore(ParseIntError),
}

impl From<ParseIntError> for PrizeError {
    fn from(problem: ParseIntError) -> PrizeError {
        PrizeError::BadScore(problem)
    }
}

fn score_at(scores: &[&str], table: usize) -> Result<u32, PrizeError> {
    println!("table {}", table);
    let typed = scores.get(table).ok_or(PrizeError::NoTable(table))?;
    let points = typed.parse::<u32>()?;
    Ok(points)
}

fn main() {
    let scores = ["9", "x"];
    println!("{:?}", score_at(&scores, 0));
    println!("{:?}", score_at(&scores, 1));
    println!("{:?}", score_at(&scores, 5));
}

What are the six output lines?

Reveal the reasoning

table 0, Ok(9), table 1, Err(BadScore(ParseIntError { kind: InvalidDigit })), table 5, Err(NoTable(5)). Table 0 holds "9", so both ? find an Ok. Table 1 holds "x": get gives Some, so the first ? passes, and parse fails; ? converts the ParseIntError with the From impl into BadScore. Table 5 does not exist: get gives None, ok_or turns it into Err(NoTable(5)), which already has the function’s error type, and the first ? returns it, so parse never runs.

Practice tickets

  1. RUST-801 · The loan desk program closes when a pupil types a word → Free
  2. RUST-802 · A mistyped minute is reported as a bad hour → Sign in · Pro
  3. RUST-803 · Returning pupils empty the coding club’s laptop cart → Sign in · Pro

Keep exploring

I/O, buffering, and external state →

Primary references