Friendly Errors in a Rust CLI with anyhow
Use anyhow's Context to turn cryptic failures like 'No such file or directory' into messages your users can actually act on.
The default failure message from a lot of Rust code is technically correct and completely unhelpful: No such file or directory (os error 2). Which file? Doing what? In a command-line tool, the error message is the user interface, so it's worth a few extra lines.
The anyhow crate is built for exactly this in application code. It gives you one catch-all Result type, and a way to attach context as an error travels up the call stack.
cargo add anyhow
Add context at each step
use std::fs;
use anyhow::{bail, Context, Result};
fn read_port(path: &str) -> Result<u16> {
let text = fs::read_to_string(path)
.with_context(|| format!("could not read config file `{path}`"))?;
let port: u16 = text
.trim()
.parse()
.with_context(|| format!("`{path}` does not contain a valid port number"))?;
if port < 1024 {
bail!("port {port} is reserved; choose 1024 or higher");
}
Ok(port)
}
fn main() -> Result<()> {
let port = read_port("port.txt")?;
println!("listening on port {port}");
Ok(())
}
Three pieces do the work:
anyhow::Result<T>is shorthand forResult<T, anyhow::Error>. Any error type that implementsstd::error::Errorconverts into it automatically, so the?operator just works..with_context(|| ...)wraps the original error with your message. The closure form only builds the string if there actually was an error.bail!returns early with a brand-new error, for problems that aren't wrapping anything, like the range check above.
What the user sees
Returning Result from main means Rust prints the error and exits with status 1. If port.txt is missing:
Error: could not read config file `port.txt`
Caused by:
No such file or directory (os error 2)
And if the file contains abc:
Error: `port.txt` does not contain a valid port number
Caused by:
invalid digit found in string
Your message comes first and the underlying cause follows. Nothing was lost, and the user now knows what to fix.
A note on where to use it
anyhow is a great fit for binaries, where you mostly want to report an error and exit. If you're writing a library, callers often need to match on specific failures. That's where a typed error, usually derived with the thiserror crate, is the better tool. Many projects use both: thiserror inside the library, anyhow in the main.rs that calls it.