RustArena

Result and the ? operator in Rust

How Rust handles errors with Result<T, E>: Ok and Err, match, the ? operator for early returns, main returning Result, and map_err.

Rust has no exceptions. A function that can fail returns Result<T, E>, an enum with two variants:

enum Result<T, E> {
    Ok(T),   // success, holding the value
    Err(E),  // failure, holding the error
}

The failure is part of the function's type, so callers can see it in the signature and the compiler makes sure they deal with it. Ignoring a Result entirely gives the warning unused Result that must be used.

Handling a Result with match

fn main() {
    for input in ["42", "forty-two"] {
        match input.parse::<i32>() {
            Ok(n) => println!("{input:?} -> {n}"),
            Err(e) => println!("{input:?} -> error: {e}"),
        }
    }
}
"42" -> 42
"forty-two" -> error: invalid digit found in string

The ? operator

Most of the time a function can't fix an error itself and should pass it to its caller. Writing a match for that each time is noisy, so Rust has ?:

let port: u16 = text.parse()?;

means: if parse returned Ok(v), give me v; if it returned Err(e), return Err(e) from this function right now. Before returning, ? converts the error with From::from, so a function returning Result<T, MyError> can use ? on any error that converts into MyError.

? only works inside a function that returns Result (or Option):

use std::fs::File;

fn main() {
    let f = File::open("config.toml")?;
    println!("{f:?}");
}
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)

main can return a Result

Change main's signature and ? works there too:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = std::fs::read_to_string("config.toml")?;
    println!("{} bytes", config.len());
    Ok(())
}

If main returns Err, Rust prints the error (in its Debug form) and exits with a non-zero code. Box<dyn Error> accepts any error type, which is convenient in small programs; applications at work often use anyhow::Result for the same purpose.

Combinators

Method Use
map(|v| ...) transform the success value
map_err(|e| ...) transform the error, e.g. into your own type or a message
and_then(|v| ...) chain another fallible step
unwrap_or(default), unwrap_or_else(|e| ...) fall back to a value
ok() turn it into an Option, dropping the error
is_ok(), is_err() test without taking it apart
expect("why") panic with a message on Err

When to panic instead

panic!, unwrap() and expect() stop the program (or thread). Use them for bugs (a broken invariant, an index you just checked) and in tests and prototypes. Use Result for anything that can go wrong in normal operation: missing files, bad input, network failures.

Common mistakes

Example

use std::num::ParseIntError;

#[derive(Debug)]
struct Order {
    item: String,
    qty: u32,
}

fn parse_qty(text: &str) -> Result<u32, ParseIntError> {
    let qty = text.trim().parse::<u32>()?;
    Ok(qty)
}

fn parse_order(line: &str) -> Result<Order, String> {
    let (item, qty) = line.split_once(',').ok_or(format!("missing comma in {line:?}"))?;
    let qty = parse_qty(qty).map_err(|e| format!("bad quantity in {line:?}: {e}"))?;
    if qty == 0 {
        return Err(format!("zero quantity in {line:?}"));
    }
    Ok(Order { item: item.trim().to_string(), qty })
}

fn total_qty(lines: &[&str]) -> Result<u32, String> {
    let mut total = 0;
    for line in lines {
        total += parse_order(line)?.qty;
    }
    Ok(total)
}

fn main() {
    for line in ["potion, 3", "sword 1", "shield, two", "arrow, 0"] {
        match parse_order(line) {
            Ok(order) => println!("ok: {} x{}", order.item, order.qty),
            Err(e) => println!("error: {e}"),
        }
    }
    println!("{:?}", total_qty(&["a, 1", "b, 2"]));
    println!("{:?}", total_qty(&["a, 1", "b, x"]));
    println!("default: {}", parse_qty("oops").unwrap_or(1));
}

Output:

ok: potion x3
error: missing comma in "sword 1"
error: bad quantity in "shield, two": invalid digit found in string
error: zero quantity in "arrow, 0"
Ok(3)
Err("bad quantity in \"b, x\": invalid digit found in string")
default: 1

Practice it