RustArena

Step 5 of 6

main returns anyhow::Result

Errors bubble up with ? until they reach main. Then what? Rust lets main itself return a Result. If it returns Err(e), Rust prints Error: followed by the error's Debug format to stderr and exits with exit code 1 instead of 0. With anyhow, the Debug format is the readable multi-line report from the last step, so you get a professional error report for free:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let text = "3\n4\n5";
    let mut total = 0;
    for (i, line) in text.lines().enumerate() {
        let n: u32 = line.parse().with_context(|| format!("line {}: not a number", i + 1))?;
        total += n;
    }
    println!("total: {total}");
    Ok(())
}
total: 12

main now ends with Ok(()): success. If text were "3\nfour\n5", nothing would go to stdout; the terminal would show this on stderr, and the shell would see exit code 1:

$ cargo run
Error: line 2: not a number

Caused by:
    invalid digit found in string
$ echo $?
1

Why this matters at work:

Reading files

The classic use: std::fs::read_to_string(path) returns io::Result<String>, and context names the file:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let path = "scores.txt";
    let text = std::fs::read_to_string(path).with_context(|| format!("could not read {path}"))?;
    println!("{} lines", text.lines().count());
    Ok(())
}

On this site, exercises can come with files: a test puts scores.txt next to your program before it runs, and another test leaves it out to check that you fail properly (exit code 1).

When you want your own format

If you need a different message format or exit codes other than 1, keep main tiny and do the work in a run function:

fn run() -> anyhow::Result<()> {
    // everything that can fail, with ? and context
    Ok(())
}

fn main() -> std::process::ExitCode {
    match run() {
        Ok(()) => std::process::ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("error: {e:#}");
            std::process::ExitCode::FAILURE
        }
    }
}

eprintln! is println! for stderr. The CLI module goes deeper into exit codes.

Common mistakes

Your task

Read scores.txt (one whole number per line; skip blank lines) and print total: <sum> and then best: <highest>. main must return anyhow::Result<()>, and every failure must exit with code 1 through ?, not a panic: a missing file (context could not read scores.txt), a line that isn't a number (context line N: bad score), and a file without any scores (no scores in scores.txt).

Challenge 2

orders.csv starts with the header line id,qty,price_cents, then one order per line. Print orders: <count> and revenue: <dollars with 2 decimals> (the sum of qty * price_cents, divided by 100). main returns anyhow::Result<()>; a missing file, a line without exactly three fields, or a field that isn't a number must exit with code 1 (use ? with context like line 3: bad qty).

Challenge 3

Keep main testable: write fn count_words(path: &str) -> anyhow::Result<usize> that reads the file and returns how many whitespace-separated words it has. If the file can't be read, the error must read (with {:#}) could not read missing.txt: No such file or directory (os error 2). The tests run with a file notes.txt present.

Previous: Context: errors that explain themselves Next: thiserror or anyhow? Both.