Skip to content

Intermediate · Error handling and collections · topic 23 of 50 · 30 min

Result and the ? operator

Explanation

Result<T, E> is an enum with two variants: Ok(T) for success and Err(E) for failure. Functions that can fail return a Result, and the caller must deal with both cases. The ? operator is a shortcut: on Ok it gives the value, on Err it returns the error from the current function at once.

Result: errors as values

Many languages use exceptions: an error "jumps" out of a function, and you hope someone catches it. In Rust, a function that can fail says so in its return type. Result is a normal enum from the standard library:

rust
enum Result<T, E> {
    Ok(T),     // success, holding the value
    Err(E),    // failure, holding the error
}
T is the type of the success value and E is the type of the error.

Because the error is part of the type, you cannot forget it. You cannot use a Result<u32, ...> as a u32; you must look inside first. Also, if you call a function that returns a Result and ignore it completely, the compiler warns: "unused Result that must be used".

Handling a Result with match

rust
use std::fs;

fn main() {
    match fs::read_to_string("prices.txt") {
        Ok(text) => println!("Read {} bytes", text.len()),
        Err(e) => println!("Could not read prices.txt: {e}"),
    }
}
read_to_string returns Result<String, std::io::Error>. Both cases are handled.

Writing your own fallible function

rust
fn parse_age(text: &str) -> Result<u8, String> {
    let age: u8 = match text.trim().parse() {
        Ok(n) => n,
        Err(_) => return Err(format!("'{text}' is not a number")),
    };
    if age < 18 {
        return Err(String::from("must be 18 or older"));
    }
    Ok(age)
}
Success is wrapped in Ok(...), failure in Err(...). Using String as the error type is fine while learning.

The ? operator

Writing match for every call gets long. The question mark operator ? does the common thing for you: if the Result is Ok, it unwraps the value; if it is Err, it returns that error from the current function immediately. It only works inside a function that itself returns a Result (or an Option).

rust
use std::fs;
use std::io;

fn read_first_line(path: &str) -> Result<String, io::Error> {
    let text = fs::read_to_string(path)?;     // on error: return Err(e) right here
    let first = text.lines().next().unwrap_or("").to_string();
    Ok(first)
}
Without ?, the first line would need a full match with an early return.

The ? also converts the error type when needed. If your function returns Result<T, MyError> and the call gives a different error type, ? uses the From trait to convert it, if a conversion exists. You will build such conversions in the next topic.

main can return a Result

rust
use std::error::Error;
use std::fs;

fn main() -> Result<(), Box<dyn Error>> {
    let text = fs::read_to_string("orders.csv")?;
    let count = text.lines().count();
    println!("{count} orders");
    Ok(())
}
Box<dyn Error> means "any kind of error". If main returns Err, Rust prints it and exits with a failure code.

Handy Result and Option helpers

HelperWhat it does
map_err(f)Change the error into another type or message
ok_or(err)Turn an Option into a Result: None becomes Err(err)
unwrap_or(default)The value, or a default on Err
is_ok() / is_err()Check without taking the value
ok()Turn a Result into an Option, dropping the error
rust
fn find_price(item: &str) -> Option<u32> {
    if item == "chai" { Some(15) } else { None }
}

fn bill(item: &str, qty: &str) -> Result<u32, String> {
    let price = find_price(item).ok_or(format!("unknown item {item}"))?;
    let qty: u32 = qty.parse().map_err(|_| format!("bad quantity {qty}"))?;
    Ok(price * qty)
}
ok_or and map_err turn different failures into the same error type, so ? works on both lines.

Common mistakes

  • Using ? in a function that returns () (like a plain main). Error: "the ? operator can only be used in a function that returns Result or Option". Change the return type.
  • Forgetting to wrap the final value: returning age instead of Ok(age).
  • Mixing error types so ? cannot convert them. Use map_err, a common error enum, or Box<dyn Error>.
  • Silencing a Result with let _ = ...; when the error actually matters.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Result and the ? operator · 1/6

Result<T, E> = Ok(T) | Err(E)

In Rust, errors are values. A function that can fail returns a Result: Ok with the value, or Err with the error.

Prefer a video by a teacher? Find free videos on “rust result error handling question mark operator” on YouTube ↗

In real life: Validating a KYC form field

A fintech app checks a PAN number typed during KYC (Know Your Customer). The check returns Result<String, String>: Ok with the cleaned number, or Err with a message to show next to the field. The screen code uses match to show either a green tick or the error. Nothing crashes, and every failure has a clear reason.

rust
fn check_pan(input: &str) -> Result<String, String> {
    let pan = input.trim().to_uppercase();
    if pan.len() != 10 {
        return Err(String::from("PAN must have 10 characters"));
    }
    let ok = pan.chars().enumerate().all(|(i, c)| match i {
        0..=4 | 9 => c.is_ascii_uppercase(),
        _ => c.is_ascii_digit(),
    });
    if ok { Ok(pan) } else { Err(String::from("PAN format is ABCDE1234F")) }
}

fn main() {
    for input in ["abcde1234f", "ABC12"] {
        match check_pan(input) {
            Ok(p) => println!("valid: {p}"),
            Err(e) => println!("error: {e}"),
        }
    }
}
Prints: valid: ABCDE1234F, then error: PAN must have 10 characters

Practice (0/3 done)

Task 1 · Question

What does this print? fn half(n: i32) -> Result<i32, String> { if n % 2 != 0 { return Err(format!("{n} is odd")); } Ok(n / 2) } fn main() { println!("{:?} {:?}", half(8), half(5)); }

Task 2 · Question

Why does this not compile? fn main() { let n: u32 = "12".parse()?; println!("{n}"); }

Task 3 · Practice

Write fn total_from_file(path: &str) -> Result<u32, Box<dyn std::error::Error>> that reads a file with one price per line and returns the sum. Use ? for reading the file and for parsing each line. Test it with a good file, a missing file and a file containing the word "ten".

Quick quiz

Question 1

1.What are the two variants of Result?

Question 2

2.What does the ? operator do on an Err?

Question 3

3.Where can you use ? ?

Question 4

4.What does Some(3).ok_or("none") give?

Question 5

5.Why does the compiler warn about an unused Result?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What are the two variants of Result?

Answer: Ok and Err

Result<T, E> is either Ok(T) or Err(E).

What does the ? operator do on an Err?

Answer: Returns the error from the current function

? propagates the error to the caller.

Where can you use ? ?

Answer: Only in functions that return Result or Option

The function must have a return type that can carry the early return.

What does Some(3).ok_or("none") give?

Answer: Ok(3)

ok_or turns Some(v) into Ok(v) and None into Err.

Why does the compiler warn about an unused Result?

Answer: Ignoring it may hide an error

Result is marked must_use so errors are not silently dropped.

What can an enum variant hold in Rust?Related

Answer: Any data: none, a tuple of values, or named fields

Rust variants can be unit-like, tuple-like or struct-like.

From Enums and Option

Which trait gives an error a human-readable message for {}?Related

Answer: Display

Display controls how {} prints a value.

From Custom error types

How does a Go function usually report failure?Related

Answer: By returning an error value as the last result

Go functions return an error as the last result; nil means success.

From Functions and multiple return values

Which verb in fmt.Errorf wraps an error so errors.Is can find it?Related

Answer: %w

%w wraps; %v and %s only copy the text.

From Errors as values: wrap, Is and As

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Error handling and collections” before its exam: Custom error types, Vec<T>: growable lists, HashMap and HashSet.
  2. Module 6 exam: Error handling and collections · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Modules, crates, traits and generics. 50 topics left in the course before the final exam.

← Previous: panic! and unrecoverable errors