RustArena

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:

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

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 {:#}:

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