Step 4 of 6
Context: errors that explain themselves
The most infamous error message in programming is No such file or directory (os error 2). Which file? What was the program trying to do? The OS error can't know; only your code does. Context is a sentence each layer adds on the way up:
use anyhow::{Context, Result};
fn read_config(path: &str) -> Result<String> {
std::fs::read_to_string(path).with_context(|| format!("could not read {path}"))
}
fn start_server() -> Result<()> {
let text = read_config("server.toml").context("loading the server config")?;
println!("config has {} bytes", text.len());
Ok(())
}
fn main() {
if let Err(e) = start_server() {
println!("{e}");
println!("---");
println!("{e:#}");
println!("---");
println!("{e:?}");
}
}
loading the server config
---
loading the server config: could not read server.toml: No such file or directory (os error 2)
---
loading the server config
Caused by:
0: could not read server.toml
1: No such file or directory (os error 2)
Piece by piece:
use anyhow::Context;imports an extension trait: it adds the methodscontextandwith_contextto everyResult(whose error is a std error or ananyhow::Error) and to everyOption..context("...")wraps the error in a new layer with your message. The original error stays inside as the cause..with_context(|| format!(...))takes a closure that builds the message only if there is an error. Use it whenever the message needsformat!, so the happy path doesn't allocate a string for nothing.- Three ways to print an
anyhow::Error:{}shows only the outermost message;{:#}shows the whole chain on one line joined by:(great for logs);{:?}shows the multi-line report withCaused by:, which is what amainreturning an error prints. e.chain()iterates over every layer as&dyn Error, outermost first, ande.root_cause()is the innermost one.
Context on Option
.context also turns None into an error with your message, which solves last step's ? on Option problem:
use anyhow::{Context, Result};
use std::collections::BTreeMap;
fn port_of(services: &BTreeMap<&str, &str>, name: &str) -> Result<u16> {
let raw = services.get(name).with_context(|| format!("no service named {name}"))?;
let port = raw.parse().with_context(|| format!("bad port {raw:?} for {name}"))?;
Ok(port)
}
fn main() {
let services = BTreeMap::from([("api", "8080"), ("db", "five")]);
for name in ["api", "db", "cache"] {
match port_of(&services, name) {
Ok(p) => println!("{name} -> {p}"),
Err(e) => println!("{e:#}"),
}
}
}
api -> 8080
bad port "five" for db: invalid digit found in string
no service named cache
Writing good context
- Say what you were doing: "loading the server config", "parsing order 1042". The inner error already says what failed.
- Include the identifiers someone needs to find the problem: file paths, line numbers, ids.
- Don't repeat the cause.
with_context(|| format!("failed: {e}"))duplicates what the chain already shows.
Common mistakes
Forgetting the import. The methods come from the trait, so without use anyhow::Context;:
fn read_count(text: &str) -> anyhow::Result<u32> {
let n = text.trim().parse::<u32>().context("reading the count")?;
Ok(n)
}
fn main() {
println!("{}", read_count("x").is_err());
}
error[E0599]: no method named `context` found for enum `Result<T, E>` in the current scope
The compiler's help even suggests use anyhow::Context;. And remember .context(format!(...)) works but builds the string even on success: prefer with_context.
Your task
Write fn load_prices(text: &str) -> Result<Vec<(String, u32)>>. Each non-empty line is item,price (trim both parts). Line numbers count from 1, including empty lines. Errors, as printed with {:#}:
- no comma:
line 2: expected item,price(usebail!) - a bad price:
line 3: bad price for tea: invalid digit found in string(add context to the parse error)
Challenge 2
Write fn user_email(users: &BTreeMap<u32, User>, id: u32) -> Result<String>. Use .with_context on the Options: a missing user gives user 7 not found, a user whose email is None gives user 7 has no email.
Challenge 3
Write fn report(e: &anyhow::Error) -> String that formats an error for a log like this, one line per layer, using e.chain():
error: loading the server config
caused by: could not read server.toml
caused by: No such file or directory (os error 2)
(No newline after the last line.)
Previous: anyhow: one error type for applications Next: main returns anyhow::Result