Guide contents

Lesson 24 of 94

Result & the ? Operator

Rust has no exceptions. An operation that can fail returns a Result, which is either a success value or an error value:

Rust
enum Result<T, E> {
    Ok(T),  // success, carrying a value of type T
    Err(E), // failure, carrying an error of type E
}

The error is part of the return type, so the caller always knows a function can fail, and the compiler warns if a Result is ignored. There is no invisible control flow: an error travels up the call stack only where you write ?.

This lesson covers everyday Result use with simple error types such as String and enums. The Error Handling section, after traits, shows how to design error types for real applications.

Concept Rust
Declaring that a function can fail Result<T, E> in the return type
Signalling an error return Err(X)
Handling an error match result { Ok(v) => ..., Err(e) => ... }
Propagating an error ? after the call
An unrecoverable bug panic!
Parsing a number "42".parse::<i32>() returns Result<i32, ParseIntError>

1. A function that can fail

Rust
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        return Err(String::from("division by zero"));
    }
    Ok(a / b)
}

fn main() {
    println!("{:?}", divide(10.0, 4.0));
    println!("{:?}", divide(1.0, 0.0));
}
Output
Ok(2.5)
Err("division by zero")

2. Handling both outcomes with match

match is an ordinary expression.

Rust
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { Err(String::from("division by zero")) } else { Ok(a / b) }
}

fn main() {
    for (a, b) in [(9.0, 3.0), (5.0, 0.0)] {
        match divide(a, b) {
            Ok(value) => println!("{a} / {b} = {value}"),
            Err(message) => println!("could not divide {a} by {b}: {message}"),
        }
    }
}
Output
9 / 3 = 3
could not divide 5 by 0: division by zero

3. Standard library functions return Result

parse returns Result<T, ParseIntError> (or another error type, depending on T). The error has a readable message when printed with {}.

Rust
fn main() {
    let inputs = ["42", "-7", "4x2", "", "99999999999"];
    for s in inputs {
        match s.parse::<i32>() {
            Ok(n) => println!("{s:?} -> {n}"),
            Err(e) => println!("{s:?} -> error: {e}"),
        }
    }
}
Output
"42" -> 42
"-7" -> -7
"4x2" -> error: invalid digit found in string
"" -> error: cannot parse integer from empty string
"99999999999" -> error: number too large to fit in target type

4. You can't use a Result as a value

Like Option, a Result<i32, _> is not an i32. You must decide what to do about the error first.

Rust✗ does not compile · E0369
fn main() {
    let quantity = "3".parse::<i32>();
    let total = quantity * 10;
    println!("{total}");
}
Output
error[E0369]: cannot multiply `Result<i32, ParseIntError>` by `{integer}`
 --> src/main.rs:3:26
  |
3 |     let total = quantity * 10;
  |                 -------- ^ -- {integer}
  |                 |
  |                 Result<i32, ParseIntError>
  |
note: `Result<i32, ParseIntError>` does not implement `Mul<{integer}>`
 --> /rustc/<version>/library/core/src/result.rs:557:0
  |
  = note: `Result<i32, ParseIntError>` is defined in another crate

Forgetting a Result entirely (calling a function and ignoring what it returns) produces a warning: unused Result that must be used. Failures can't be silently dropped.

5. unwrap and expect panic on Err

They take the Ok value or panic with the error. Use them in tests, examples, and when failure is truly impossible.

Rustpanics at runtime
fn main() {
    let port: u16 = "8080".parse().unwrap();
    println!("port {port}");
    let workers: u8 = "300".parse().expect("WORKERS must fit in a u8");
    println!("{workers}");
}
Output
port 8080

thread 'main' panicked at src/main.rs:4:37:
WORKERS must fit in a u8: ParseIntError { kind: PosOverflow }

6. Falling back to a default

Rust
fn main() {
    let a: u32 = "12".parse().unwrap_or(1);
    let b: u32 = "twelve".parse().unwrap_or(1);
    let c: u32 = "twelve".parse().unwrap_or_default();
    let d: u32 = "twelve".parse().unwrap_or_else(|e| {
        println!("  bad input ({e}), using 10");
        10
    });
    println!("{a} {b} {c} {d}");
}
Output
  bad input (invalid digit found in string), using 10
12 1 0 10

unwrap_or_else receives the error, so the fallback can log it or depend on it.

7. The ? operator propagates errors

In a function that returns Result, expr? means: "if expr is Err(e), return Err(e) from this function immediately; otherwise, give me the Ok value". The propagation is visible at each call site.

Rust
use std::num::ParseIntError;

fn sum_of(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = a.parse::<i32>()?;
    let y = b.parse::<i32>()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", sum_of("20", "22"));
    println!("{:?}", sum_of("20", "twenty-two"));
}
Output
Ok(42)
Err(ParseIntError { kind: InvalidDigit })

use std::num::ParseIntError; brings a type from the standard library into scope.

8. What ? expands to

? is short for a match that returns early. Writing it out once makes it less magical.

Rust
use std::num::ParseIntError;

fn double(s: &str) -> Result<i32, ParseIntError> {
    let n = match s.parse::<i32>() {
        Ok(value) => value,
        Err(e) => return Err(e.into()), // `?` also converts the error with `into()`
    };
    Ok(n * 2)
}

fn main() {
    println!("{:?} {:?}", double("21"), double("x"));
}
Output
Ok(42) Err(ParseIntError { kind: InvalidDigit })

The .into() conversion is what lets ? turn one error type into another, which Error Handling uses heavily.

9. ? only works in functions that return Result (or Option)

Rust✗ does not compile · E0277
fn main() {
    let n: i32 = "5".parse()?;
    println!("{n}");
}
Output
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:2:29
  |
1 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
2 |     let n: i32 = "5".parse()?;
  |                             ^ cannot use the `?` operator in a function that returns `()`
  |
help: consider adding return type
  |
1 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
2 |     let n: i32 = "5".parse()?;
3 |     println!("{n}");
4 +     Ok(())
  |

10. main can return a Result

When main returns Result<(), E>, you can use ? in it. Returning an Err prints Error: ... (the Debug format of the error) and exits with code 1.

Rustexits with an error
fn parse_percent(s: &str) -> Result<u8, String> {
    let n: u8 = s.parse().map_err(|_| format!("'{s}' is not a number"))?;
    if n > 100 {
        return Err(format!("{n} is more than 100"));
    }
    Ok(n)
}

fn main() -> Result<(), String> {
    let a = parse_percent("75")?;
    println!("a = {a}%");
    let b = parse_percent("250")?;
    println!("b = {b}%");
    Ok(())
}
Output
a = 75%
Error: "250 is more than 100"

The badge exits with an error marks examples like this, which end by returning an error from main. The output shows standard error too. Ok(()) means "success, with no value".

11. map_err converts the error type

? needs the error types to line up. map_err transforms the error, for example into a message with more context.

Rust
fn read_age(input: &str) -> Result<u8, String> {
    let age: u8 = input
        .trim()
        .parse()
        .map_err(|e| format!("age '{input}' is invalid: {e}"))?;
    if age < 13 {
        return Err(format!("age {age} is below the minimum of 13"));
    }
    Ok(age)
}

fn main() {
    for input in [" 34 ", "abc", "300", "9"] {
        println!("{:?}", read_age(input));
    }
}
Output
Ok(34)
Err("age 'abc' is invalid: invalid digit found in string")
Err("age '300' is invalid: number too large to fit in target type")
Err("age 9 is below the minimum of 13")

12. Mixing error types without conversion fails

Two different error types can't flow through the same ? into a Result<_, String> without conversion.

Rust✗ does not compile · E0277
fn load(port: &str, ratio: &str) -> Result<(u16, f64), String> {
    let p: u16 = port.parse()?;
    let r: f64 = ratio.parse()?;
    Ok((p, r))
}

fn main() {
    println!("{:?}", load("80", "0.5"));
}
Output
error[E0277]: `?` couldn't convert the error to `String`
 --> src/main.rs:2:30
  |
1 | fn load(port: &str, ratio: &str) -> Result<(u16, f64), String> {
  |                                     -------------------------- expected `String` because of this
2 |     let p: u16 = port.parse()?;
  |                       -------^ the trait `From<ParseIntError>` is not implemented for `String`
  |                       |
  |                       this can't be annotated with `?` because it has type `Result<_, ParseIntError>`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = help: the following other types implement trait `From<T>`:
            `String` implements `From<&String>`
            `String` implements `From<&mut str>`
            `String` implements `From<&str>`
            `String` implements `From<Box<str>>`
            `String` implements `From<Cow<'_, str>>`
            `String` implements `From<char>`

The fix is .map_err(|e| e.to_string())? on each call, or an error type that knows how to convert from both, covered in Error Handling.

13. map and and_then on Result

They work like their Option counterparts: map changes the success value, and_then chains another fallible step.

Rust
fn parse(s: &str) -> Result<i64, String> {
    s.parse::<i64>().map_err(|e| e.to_string())
}

fn non_negative(n: i64) -> Result<i64, String> {
    if n >= 0 { Ok(n) } else { Err(format!("{n} is negative")) }
}

fn main() {
    for s in ["16", "-4", "abc"] {
        let r = parse(s).and_then(non_negative).map(|n| (n as f64).sqrt());
        println!("{s:>4} -> {:?}", r);
    }
}
Output
  16 -> Ok(4.0)
  -4 -> Err("-4 is negative")
 abc -> Err("invalid digit found in string")

14. Converting between Option and Result

ok_or turns None into an error. ok() turns a Result into an Option, dropping the error.

Rust
fn find_price(catalog: &[(&str, u32)], sku: &str) -> Result<u32, String> {
    catalog
        .iter()
        .find(|(s, _)| *s == sku)
        .map(|(_, price)| *price)
        .ok_or(format!("unknown SKU {sku}"))
}

fn main() {
    let catalog = [("KB-1", 4_999), ("MS-2", 1_999)];
    println!("{:?}", find_price(&catalog, "MS-2"));
    println!("{:?}", find_price(&catalog, "XX-9"));
    println!("{:?}", "12".parse::<u8>().ok());
}
Output
Ok(1999)
Err("unknown SKU XX-9")
Some(12)

Use ok_or_else(|| ...) when building the error is expensive, so it only happens on None.

15. ? on an Option inside a Result function

? can't mix the two directly. Convert the Option first with ok_or.

Rust
fn first_word_length(text: &str) -> Result<usize, String> {
    let word = text.split_whitespace().next().ok_or("text is empty")?;
    Ok(word.len())
}

fn main() {
    println!("{:?}", first_word_length("hello world"));
    println!("{:?}", first_word_length("   "));
}
Output
Ok(5)
Err("text is empty")

ok_or("text is empty") gives a &str error, and ? converts it into the function's String error type with into().

16. Inspecting without extracting

Rust
fn main() {
    let good: Result<u32, String> = Ok(3);
    let bad: Result<u32, String> = Err(String::from("boom"));
    println!("{} {}", good.is_ok(), bad.is_err());
    println!("{:?} {:?}", good.as_ref().ok(), bad.as_ref().err());
    if let Err(e) = &bad {
        println!("error was: {e}");
    }
    println!("{}", good.is_ok_and(|n| n > 2)); // consumes `good`, so it comes last
}
Output
true true
Some(3) Some("boom")
error was: boom
true

is_ok_and takes the Result by value, like most Result and Option methods that hand you the inner value. as_ref() turns a &Result<T, E> into a Result<&T, &E> so you can inspect it without giving it up.

17. Result<(), E> for operations with no value

An operation that either succeeds or fails, with nothing to return on success, returns Result<(), E>.

Rust
fn validate_username(name: &str) -> Result<(), String> {
    if name.len() < 3 {
        return Err(String::from("too short"));
    }
    if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
        return Err(String::from("only letters, digits, and _ are allowed"));
    }
    Ok(())
}

fn main() {
    for name in ["ferris_99", "ab", "bad name!"] {
        match validate_username(name) {
            Ok(()) => println!("{name}: ok"),
            Err(e) => println!("{name}: {e}"),
        }
    }
}
Output
ferris_99: ok
ab: too short
bad name!: only letters, digits, and _ are allowed

18. Enums as error types

A String error is fine for messages, but the caller can't easily react to which error happened. An enum makes every failure case explicit and matchable.

Rust
#[derive(Debug)]
enum WithdrawError {
    InsufficientFunds { needed: u64, available: u64 },
    AccountFrozen,
    AmountTooLarge(u64),
}

fn withdraw(balance: u64, frozen: bool, amount: u64) -> Result<u64, WithdrawError> {
    if frozen {
        return Err(WithdrawError::AccountFrozen);
    }
    if amount > 10_000 {
        return Err(WithdrawError::AmountTooLarge(amount));
    }
    if amount > balance {
        return Err(WithdrawError::InsufficientFunds { needed: amount, available: balance });
    }
    Ok(balance - amount)
}

fn main() {
    let attempts = [(500, false, 200), (500, false, 900), (500, true, 10), (50_000, false, 20_000)];
    for (balance, frozen, amount) in attempts {
        match withdraw(balance, frozen, amount) {
            Ok(left) => println!("ok, {left} left"),
            Err(WithdrawError::InsufficientFunds { needed, available }) => {
                println!("short by {}", needed - available)
            }
            Err(e) => println!("refused: {:?}", e),
        }
    }
}
Output
ok, 300 left
short by 400
refused: AccountFrozen
refused: AmountTooLarge(20000)

One enum describes all the failure cases, and match makes sure every case is considered.

19. Collecting results: all or nothing

Collecting an iterator of Results into Result<Vec<T>, E> stops at the first error. This is how you parse a whole list and fail if any item is bad.

Rust
fn main() {
    let good = ["1", "2", "3"];
    let bad = ["1", "two", "3", "four"];
    let a: Result<Vec<i32>, _> = good.iter().map(|s| s.parse::<i32>()).collect();
    let b: Result<Vec<i32>, _> = bad.iter().map(|s| s.parse::<i32>()).collect();
    println!("{:?}", a);
    println!("{:?}", b);
}
Output
Ok([1, 2, 3])
Err(ParseIntError { kind: InvalidDigit })

20. Separating successes from failures

Sometimes you want to keep going and report every failure. partition splits the results into two lists.

Rust
fn main() {
    let inputs = ["10", "x", "30", "-", "50"];
    let (oks, errs): (Vec<_>, Vec<_>) = inputs.iter().map(|s| s.parse::<i32>()).partition(|r| r.is_ok());
    let numbers: Vec<i32> = oks.into_iter().map(Result::unwrap).collect();
    println!("parsed: {:?}", numbers);
    println!("{} failures", errs.len());
}
Output
parsed: [10, 30, 50]
2 failures

21. ? inside a loop

? works anywhere in the function body, including loops. The first error stops the whole function.

Rust
fn total_cents(prices: &[&str]) -> Result<u64, String> {
    let mut total = 0;
    for (i, p) in prices.iter().enumerate() {
        let (dollars, cents) = p.split_once('.').ok_or(format!("line {}: missing '.' in {p}", i + 1))?;
        let d: u64 = dollars.parse().map_err(|_| format!("line {}: bad dollars in {p}", i + 1))?;
        let c: u64 = cents.parse().map_err(|_| format!("line {}: bad cents in {p}", i + 1))?;
        total += d * 100 + c;
    }
    Ok(total)
}

fn main() {
    println!("{:?}", total_cents(&["4.50", "10.99", "0.51"]));
    println!("{:?}", total_cents(&["4.50", "10", "0.51"]));
    println!("{:?}", total_cents(&["4.50", "ten.99"]));
}
Output
Ok(1600)
Err("line 2: missing '.' in 10")
Err("line 2: bad dollars in ten.99")

22. When to panic and when to return Result

  • Return Result when failure is a normal, expected outcome the caller may want to handle: bad input, a missing file, a network timeout, insufficient funds.
  • Panic when failure means the program has a bug: an invariant was broken, an index is out of range that "can't be", an internal state is impossible.
Rust
fn parse_quantity(input: &str) -> Result<u32, String> {
    // user input: failure is expected, so return a Result
    input.trim().parse().map_err(|_| format!("'{input}' is not a quantity"))
}

fn unit_price(prices: &[u32], index: usize) -> u32 {
    // our own table: a bad index is a bug in our code, so panicking is right
    prices[index]
}

fn main() {
    let prices = [250, 1_200];
    match parse_quantity(" 3 ") {
        Ok(q) => println!("total {}", q * unit_price(&prices, 1)),
        Err(e) => println!("{e}"),
    }
    match parse_quantity("three") {
        Ok(q) => println!("total {}", q * unit_price(&prices, 0)),
        Err(e) => println!("{e}"),
    }
}
Output
total 3600
'three' is not a quantity

23. Realistic example: parsing a configuration file

Each error names the line it came from, so the user knows exactly what to fix.

Rust
#[derive(Debug)]
struct Config {
    host: String,
    port: u16,
    workers: u8,
}

fn parse_config(text: &str) -> Result<Config, String> {
    let mut host = None;
    let mut port = None;
    let mut workers = None;
    for (n, raw) in text.lines().enumerate() {
        let line = raw.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let (key, value) = line.split_once('=').ok_or(format!("line {}: expected key=value", n + 1))?;
        let (key, value) = (key.trim(), value.trim());
        match key {
            "host" => host = Some(value.to_string()),
            "port" => port = Some(value.parse::<u16>().map_err(|e| format!("line {}: port: {e}", n + 1))?),
            "workers" => workers = Some(value.parse::<u8>().map_err(|e| format!("line {}: workers: {e}", n + 1))?),
            other => return Err(format!("line {}: unknown key '{other}'", n + 1)),
        }
    }
    Ok(Config {
        host: host.ok_or("missing 'host'")?,
        port: port.unwrap_or(8080),
        workers: workers.unwrap_or(4),
    })
}

fn main() {
    let good = "# server\nhost = example.com\nport = 443\n";
    let bad_port = "host = example.com\nport = 99999";
    let unknown = "host = a\ncolour = blue";
    let missing = "port = 80";
    for text in [good, bad_port, unknown, missing] {
        println!("{:?}", parse_config(text));
    }
}
Output
Ok(Config { host: "example.com", port: 443, workers: 4 })
Err("line 2: port: number too large to fit in target type")
Err("line 2: unknown key 'colour'")
Err("missing 'host'")

24. Realistic example: a registration pipeline

Each step returns a Result, and ? chains them. The first failing step decides the error, and every exit point is visible.

Rust
#[derive(Debug)]
struct NewUser {
    email: String,
    password_hash: String,
}

fn validate_email(email: &str) -> Result<String, String> {
    let email = email.trim().to_lowercase();
    let (local, domain) = email.split_once('@').ok_or("email must contain '@'")?;
    if local.is_empty() || !domain.contains('.') {
        return Err(format!("'{email}' is not a valid email"));
    }
    Ok(email)
}

fn check_password(password: &str) -> Result<(), String> {
    if password.len() < 12 {
        return Err(String::from("password must be at least 12 characters"));
    }
    if !password.chars().any(|c| c.is_ascii_digit()) {
        return Err(String::from("password must contain a digit"));
    }
    Ok(())
}

fn hash(password: &str) -> String {
    format!("hash({})", password.len()) // stand-in for a real password hash
}

fn register(existing: &[&str], email: &str, password: &str) -> Result<NewUser, String> {
    let email = validate_email(email)?;
    if existing.contains(&email.as_str()) {
        return Err(format!("{email} is already registered"));
    }
    check_password(password)?;
    Ok(NewUser { email, password_hash: hash(password) })
}

fn main() {
    let existing = ["ana@example.com"];
    let attempts = [
        ("Ben@Example.com", "correct-horse-7"),
        ("ana@example.com", "another-password-1"),
        ("not-an-email", "whatever-123456"),
        ("cy@example.com", "short1"),
        ("dee@example.com", "no-digits-at-all"),
    ];
    for (email, pw) in attempts {
        match register(&existing, email, pw) {
            Ok(user) => println!("registered {:?}", user),
            Err(e) => println!("rejected: {e}"),
        }
    }
}
Output
registered NewUser { email: "ben@example.com", password_hash: "hash(15)" }
rejected: ana@example.com is already registered
rejected: email must contain '@'
rejected: password must be at least 12 characters
rejected: password must contain a digit

25. Realistic example: parsing dimensions like 1920x1080

Rust
#[derive(Debug)]
enum DimError {
    MissingSeparator,
    BadNumber(String),
    Zero,
}

fn parse_dimensions(s: &str) -> Result<(u32, u32), DimError> {
    let (w, h) = s.split_once('x').ok_or(DimError::MissingSeparator)?;
    let w: u32 = w.trim().parse().map_err(|_| DimError::BadNumber(w.to_string()))?;
    let h: u32 = h.trim().parse().map_err(|_| DimError::BadNumber(h.to_string()))?;
    if w == 0 || h == 0 {
        return Err(DimError::Zero);
    }
    Ok((w, h))
}

fn main() {
    for s in ["1920x1080", "800 x 600", "1024", "wide x 10", "0x100"] {
        println!("{s:>10} -> {:?}", parse_dimensions(s));
    }
}
Output
 1920x1080 -> Ok((1920, 1080))
 800 x 600 -> Ok((800, 600))
      1024 -> Err(MissingSeparator)
 wide x 10 -> Err(BadNumber("wide "))
     0x100 -> Err(Zero)

26. Realistic example: retrying only some errors

Because errors are values, deciding what to do based on the kind of error is just a match. Here, timeouts are retried and everything else fails immediately.

Rust
#[derive(Debug)]
enum FetchError {
    Timeout,
    NotFound,
    Unauthorized,
}

fn fetch(attempt: u32, url: &str) -> Result<String, FetchError> {
    match url {
        "/flaky" if attempt < 3 => Err(FetchError::Timeout),
        "/flaky" | "/ok" => Ok(format!("200 OK from {url}")),
        "/secret" => Err(FetchError::Unauthorized),
        _ => Err(FetchError::NotFound),
    }
}

fn fetch_with_retry(url: &str, max_attempts: u32) -> Result<String, FetchError> {
    let mut attempt = 1;
    loop {
        match fetch(attempt, url) {
            Err(FetchError::Timeout) if attempt < max_attempts => {
                println!("  {url}: timeout on attempt {attempt}, retrying");
                attempt += 1;
            }
            other => return other,
        }
    }
}

fn main() {
    for url in ["/ok", "/flaky", "/secret", "/missing"] {
        println!("{url}: {:?}", fetch_with_retry(url, 5));
    }
    println!("/flaky with 2 attempts: {:?}", fetch_with_retry("/flaky", 2));
}
Output
/ok: Ok("200 OK from /ok")
  /flaky: timeout on attempt 1, retrying
  /flaky: timeout on attempt 2, retrying
/flaky: Ok("200 OK from /flaky")
/secret: Err(Unauthorized)
/missing: Err(NotFound)
  /flaky: timeout on attempt 1, retrying
/flaky with 2 attempts: Err(Timeout)

27. Realistic example: a transfer that validates before changing anything

All checks happen first, using ?. Only when every check has passed are the balances changed, so a failure never leaves the accounts half-updated.

Rust
#[derive(Debug)]
struct Account {
    id: u32,
    balance: i64,
}

fn find_index(accounts: &[Account], id: u32) -> Result<usize, String> {
    accounts.iter().position(|a| a.id == id).ok_or(format!("account {id} not found"))
}

fn transfer(accounts: &mut [Account], from: u32, to: u32, amount: i64) -> Result<(), String> {
    if amount <= 0 {
        return Err(format!("amount must be positive, got {amount}"));
    }
    let from_i = find_index(accounts, from)?;
    let to_i = find_index(accounts, to)?;
    if from_i == to_i {
        return Err(String::from("cannot transfer to the same account"));
    }
    if accounts[from_i].balance < amount {
        return Err(format!("account {from} has {} but needs {amount}", accounts[from_i].balance));
    }
    accounts[from_i].balance -= amount;
    accounts[to_i].balance += amount;
    Ok(())
}

fn main() {
    let mut accounts = vec![Account { id: 1, balance: 100 }, Account { id: 2, balance: 50 }];
    println!("{:?}", transfer(&mut accounts, 1, 2, 30));
    println!("{:?}", transfer(&mut accounts, 1, 3, 30));
    println!("{:?}", transfer(&mut accounts, 2, 1, 500));
    println!("{:?}", transfer(&mut accounts, 1, 1, 5));
    println!("{:?}", accounts);
}
Output
Ok(())
Err("account 3 not found")
Err("account 2 has 80 but needs 500")
Err("cannot transfer to the same account")
[Account { id: 1, balance: 70 }, Account { id: 2, balance: 80 }]

position returns the index of the first element matching the closure, as an Option.

28. Realistic example: a command-line tool's main

A typical small program: main returns Result, every step uses ?, and any error ends the program with a message and a non-zero exit code, which scripts and CI systems rely on.

Rustexits with an error
fn parse_args(args: &[&str]) -> Result<(String, u32), String> {
    let [name, count] = args else {
        return Err(format!("expected 2 arguments, got {}", args.len()));
    };
    let count: u32 = count.parse().map_err(|_| format!("count must be a number, got '{count}'"))?;
    Ok((name.to_string(), count))
}

fn run(args: &[&str]) -> Result<(), String> {
    let (name, count) = parse_args(args)?;
    for i in 1..=count {
        println!("{i}: hello, {name}");
    }
    Ok(())
}

fn main() -> Result<(), String> {
    run(&["ferris", "2"])?;
    run(&["ferris", "two"])?;
    println!("never printed");
    Ok(())
}
Output
1: hello, ferris
2: hello, ferris
Error: "count must be a number, got 'two'"

Real programs read arguments with std::env::args(). Keeping the logic in a run function that takes the arguments makes it easy to test.

Summary

  • Result<T, E> is Ok(value) or Err(error). Errors are ordinary values in the return type.
  • Handle results with match, if let, unwrap_or, unwrap_or_else, map, and_then, and map_err.
  • ? returns the error early from a function returning Result (or Option), converting it with into().
  • main can return Result<(), E> to use ? and exit with a non-zero code on error.
  • Convert with ok_or (Option to Result) and ok() (Result to Option).
  • Collect into Result<Vec<T>, E> for all-or-nothing processing.
  • Use Result for expected failures and panic! for bugs.

This completes the Structs, Enums & Pattern Matching section. Next, the section on Lifetimes, which finishes the story of references.