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:
- Exit codes are how scripts, CI pipelines and other programs know whether your tool worked. A panic exits with 101 and a scary message;
Errfrommainexits with 1 and your report. - stderr keeps errors out of the real output, so
mytool > report.txtstill shows errors on screen and doesn't put them in the file. - Inside
main,?works like anywhere else. Nounwrapneeded for input you don't control.
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
unwrapinmainbecause "it's just main": a missing file then becomes a panic with exit code 101 and a message aboutcalled Result::unwrap() on an Err value, not the clear report above.- Printing the error and returning
Ok(()): the program reports a failure but exits with 0, so scripts think it worked. - Forgetting
Ok(())at the end ofmain:error[E0308]: mismatched types, expectedResult<(), Error>, found().
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.