Step 3 of 6
anyhow: one error type for applications
Typed errors are for code whose callers need to tell failures apart. But in an application, most errors travel up to main and get reported to a human or a log. Writing an enum for every function there is busywork that buys nothing. anyhow gives you one error type, anyhow::Error, that can hold any error:
use anyhow::{anyhow, bail, ensure, Result};
fn parse_reading(line: &str) -> Result<(String, f64)> {
let Some((name, value)) = line.split_once('=') else {
bail!("expected sensor=value, got {line:?}");
};
ensure!(!name.trim().is_empty(), "sensor name is empty");
let celsius: f64 = value.trim().parse()?;
if !(-50.0..=60.0).contains(&celsius) {
return Err(anyhow!("{celsius} C is out of range"));
}
Ok((name.trim().to_string(), celsius))
}
fn main() {
for line in ["porch=12.5", "porch", "=3", "attic=hot", "oven=220"] {
match parse_reading(line) {
Ok((name, c)) => println!("{name}: {c}"),
Err(e) => println!("error: {e}"),
}
}
}
porch: 12.5
error: expected sensor=value, got "porch"
error: sensor name is empty
error: invalid float literal
error: 220 C is out of range
Piece by piece:
anyhow::Result<T>is short forResult<T, anyhow::Error>. Importing it asResultis the convention in application code.?works on any error type that implementsstd::error::Error(plusSend + Sync + 'static, which almost all do): here aParseFloatErroris converted intoanyhow::Errorautomatically. No enum, noFromimpls.anyhow!("...")creates an error from a message, withformat!syntax.bail!("...")is short forreturn Err(anyhow!("...")).ensure!(condition, "...")is short forif !condition { bail!("...") }.- The original error is kept inside, not just its text. The next steps use that: adding context and finding out what the cause was.
anyhow vs Box<dyn Error>
You've used Box<dyn std::error::Error> for the same purpose. anyhow is the production version of that idea: it requires Send + Sync so errors can cross threads and async tasks, it's a single pointer wide, it records a backtrace when RUST_BACKTRACE=1 is set, and it adds context (next step).
Common mistakes
? on an Option doesn't produce an error by itself, because None carries no message:
use std::collections::HashMap;
fn port(settings: &HashMap<String, u16>) -> anyhow::Result<u16> {
let p = settings.get("port")?;
Ok(*p)
}
fn main() {
println!("{:?}", port(&HashMap::new()).is_err());
}
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
Give it a message: settings.get("port").ok_or_else(|| anyhow!("no port setting"))?, or with .context(...) from the next step.
- anyhow in a library's public API. Callers get one opaque type and can't
matchon what went wrong. Libraries return thiserror enums; applications use anyhow. - Replacing the cause:
.map_err(|_| anyhow!("bad config"))throws away the original error and its message. Add context instead (next step).
Your task
Write fn parse_duration(s: &str) -> Result<u64> (anyhow's Result) that turns "90s", "5m" or "2h" into seconds. Use these exact errors:
- empty input:
bail!withempty duration - last character not
s,morh:unknown unit 'x' in "10x"(Debug-format the char and the input) - the number doesn't parse: just use
?(the message is theParseIntError's) - a zero amount:
ensure!withduration must be positive
Challenge 2
Write fn average(values: &[&str]) -> anyhow::Result<f64> that parses every value as f64 (trimmed) and returns their average. An empty slice is an error with the message no values to average (use ensure!); a value that doesn't parse returns the parse error with ?.
Challenge 3
Write a tiny calculator. Each line of standard input is <a> <op> <b> with whole numbers (i64) and op one of + - * /. Put the logic in fn eval(line: &str) -> anyhow::Result<i64> and print = <result> or error: <message> for each line. Errors: wrong number of parts gives expected: <a> <op> <b>; an unknown operator gives unknown operator "%"; dividing by zero gives division by zero; numbers that don't parse use ?; results that overflow give overflow (use the checked_ methods).
Previous: Wrapping causes: #[from], #[source], transparent Next: Context: errors that explain themselves