Lesson 50 of 94
thiserror & anyhow
The previous lesson wrote error types by hand: an enum, a Display impl, an Error impl, source(), and From conversions. It works, but it's a lot of boilerplate. Two small, ubiquitous crates remove it:
thiserrorderives all of that from attributes. Use it for libraries and any code whose callers need to match on specific errors.anyhowprovides one flexible error type with easy context messages. Use it for applications, where errors are mostly reported rather than matched.
A common setup is thiserror in your library crates and anyhow in your binary.
| Task | Rust |
|---|---|
| Defining an error type | #[derive(thiserror::Error)] on an enum |
| Adding context to an error | .context("loading config") |
| Returning an error early | bail!(msg) |
| Checking a condition | ensure!(cond, msg) |
| Any error as the result type | anyhow::Result<T> |
Setup
Add both crates to Cargo.toml:
cargo add thiserror anyhow[dependencies]
anyhow = "1"
thiserror = "2"Part 1: thiserror
1. Deriving an error enum
#[derive(Error)] implements std::error::Error. Each #[error("...")] attribute becomes that variant's Display message.
use thiserror::Error;
#[derive(Debug, Error)]
enum PaymentError {
#[error("card was declined")]
Declined,
#[error("card expired")]
Expired,
#[error("payment provider is unavailable")]
ProviderDown,
}
fn charge(card: &str) -> Result<String, PaymentError> {
match card {
"4242" => Ok(String::from("ch_1")),
"0000" => Err(PaymentError::Declined),
"1111" => Err(PaymentError::Expired),
_ => Err(PaymentError::ProviderDown),
}
}
fn main() {
for card in ["4242", "0000", "1111", "9999"] {
match charge(card) {
Ok(id) => println!("{card}: charged {id}"),
Err(e) => println!("{card}: {e}"),
}
}
}4242: charged ch_1
0000: card was declined
1111: card expired
9999: payment provider is unavailable2. Including fields in the message
Tuple fields are referred to as {0}, {1}; named fields by name. Format specs such as {:?} and {:.2} work too.
use thiserror::Error;
#[derive(Debug, Error)]
enum InventoryError {
#[error("unknown SKU {0}")]
UnknownSku(String),
#[error("only {available} of {sku} left, {requested} requested")]
Insufficient { sku: String, requested: u32, available: u32 },
#[error("price {0:.2} is below the minimum")]
PriceTooLow(f64),
}
fn main() {
let errors = [
InventoryError::UnknownSku(String::from("XX-9")),
InventoryError::Insufficient { sku: String::from("KB-1"), requested: 5, available: 2 },
InventoryError::PriceTooLow(0.4999),
];
for e in &errors {
println!("{e}");
}
}unknown SKU XX-9
only 2 of KB-1 left, 5 requested
price 0.50 is below the minimum3. #[from]: automatic conversions for ?
#[from] generates From<Inner> for YourError, so ? wraps the inner error, and it also sets the variant's source().
use std::num::{ParseFloatError, ParseIntError};
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("invalid integer")]
Int(#[from] ParseIntError),
#[error("invalid decimal")]
Float(#[from] ParseFloatError),
#[error("missing setting '{0}'")]
Missing(&'static str),
}
fn load(workers: Option<&str>, ratio: &str) -> Result<(u32, f64), ConfigError> {
let w = workers.ok_or(ConfigError::Missing("workers"))?.parse()?;
let r = ratio.parse()?;
Ok((w, r))
}
fn main() {
use std::error::Error;
println!("{:?}", load(Some("4"), "0.5"));
for (w, r) in [(Some("four"), "0.5"), (Some("4"), "half"), (None, "0.5")] {
let e = load(w, r).unwrap_err();
println!("{e} (source: {:?})", e.source().map(|s| s.to_string()));
}
}Ok((4, 0.5))
invalid integer (source: Some("invalid digit found in string"))
invalid decimal (source: Some("invalid float literal"))
missing setting 'workers' (source: None)Compare this with example 7 of Custom Error Types: about 30 lines of impl blocks are gone.
4. #[source]: a cause without automatic conversion
When a variant holds a cause but also other fields (so From wouldn't make sense), mark the cause with #[source]. A field named source is treated as the source automatically.
use std::error::Error;
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Debug, Error)]
enum ImportError {
#[error("line {line}: invalid quantity '{value}'")]
BadQuantity {
line: usize,
value: String,
#[source]
cause: ParseIntError,
},
#[error("line {line}: expected 2 columns, found {found}")]
Columns { line: usize, found: usize },
}
fn parse(csv: &str) -> Result<Vec<(String, u32)>, ImportError> {
csv.lines()
.enumerate()
.map(|(i, l)| {
let cols: Vec<&str> = l.split(',').collect();
let [name, qty] = cols[..] else {
return Err(ImportError::Columns { line: i + 1, found: cols.len() });
};
let qty = qty.trim().parse().map_err(|cause| ImportError::BadQuantity { line: i + 1, value: qty.to_string(), cause })?;
Ok((name.to_string(), qty))
})
.collect()
}
fn main() {
println!("{:?}", parse("pen,3\nink,5"));
let e = parse("pen,3\nink,five").unwrap_err();
println!("{e}\n caused by: {}", e.source().unwrap());
println!("{}", parse("pen").unwrap_err());
}Ok([("pen", 3), ("ink", 5)])
line 2: invalid quantity 'five'
caused by: invalid digit found in string
line 1: expected 2 columns, found 15. #[error(transparent)]: pass another error's message through
A transparent variant forwards Display and source to the inner error. It's useful for an "other" catch-all, or to wrap errors from a lower layer unchanged.
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("user {0} not found")]
NotFound(u32),
#[error(transparent)]
Parse(#[from] std::num::ParseIntError),
}
fn find_user(raw_id: &str) -> Result<String, AppError> {
let id: u32 = raw_id.parse()?;
if id == 1 { Ok(String::from("ada")) } else { Err(AppError::NotFound(id)) }
}
fn main() {
for raw in ["1", "42", "abc"] {
match find_user(raw) {
Ok(u) => println!("{raw}: {u}"),
Err(e) => println!("{raw}: {e}"),
}
}
}1: ada
42: user 42 not found
abc: invalid digit found in string6. Struct errors
thiserror works on structs as well as enums.
use thiserror::Error;
#[derive(Debug, Error)]
#[error("rate limit exceeded for {client}: {count} requests in {window_secs}s (max {max})")]
struct RateLimited {
client: String,
count: u32,
max: u32,
window_secs: u32,
}
fn check(client: &str, count: u32) -> Result<(), RateLimited> {
if count > 100 {
return Err(RateLimited { client: client.to_string(), count, max: 100, window_secs: 60 });
}
Ok(())
}
fn main() {
println!("{:?}", check("app-1", 20));
println!("{}", check("app-2", 250).unwrap_err());
}Ok(())
rate limit exceeded for app-2: 250 requests in 60s (max 100)7. Matching on thiserror variants
The derived type is an ordinary enum, so callers match on it exactly as before.
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
enum LoginError {
#[error("unknown user")]
UnknownUser,
#[error("wrong password ({attempts_left} attempts left)")]
WrongPassword { attempts_left: u8 },
#[error("account locked")]
Locked,
}
fn login(user: &str, password: &str, failures: u8) -> Result<(), LoginError> {
if user != "ada" {
return Err(LoginError::UnknownUser);
}
if failures >= 3 {
return Err(LoginError::Locked);
}
if password != "correct horse" {
return Err(LoginError::WrongPassword { attempts_left: 2 - failures });
}
Ok(())
}
fn main() {
let attempts = [("bob", "x", 0), ("ada", "guess", 0), ("ada", "guess", 2), ("ada", "correct horse", 3), ("ada", "correct horse", 1)];
for (user, pw, failures) in attempts {
let response = match login(user, pw, failures) {
Ok(()) => String::from("welcome"),
Err(LoginError::WrongPassword { attempts_left: 0 }) => String::from("last attempt failed, locking account"),
Err(e @ LoginError::Locked) => format!("{e}: contact support"),
Err(e) => e.to_string(),
};
println!("{user}: {response}");
}
}bob: unknown user
ada: wrong password (2 attempts left)
ada: last attempt failed, locking account
ada: account locked: contact support
ada: welcomePart 2: anyhow
8. anyhow::Result and ? on any error
anyhow::Result<T> is Result<T, anyhow::Error>. anyhow::Error can hold any error type, so ? works on everything, like Box<dyn Error> but with better features.
use anyhow::Result;
fn parse_pair(input: &str) -> Result<(u32, f64)> {
let (a, b) = input.split_once(',').ok_or_else(|| anyhow::anyhow!("expected 'int,float'"))?;
Ok((a.trim().parse()?, b.trim().parse()?))
}
fn main() {
for input in ["3, 2.5", "3", "x, 2.5", "3, y"] {
match parse_pair(input) {
Ok(p) => println!("{input:>7}: {:?}", p),
Err(e) => println!("{input:>7}: {e}"),
}
}
} 3, 2.5: (3, 2.5)
3: expected 'int,float'
x, 2.5: invalid digit found in string
3, y: invalid float literal9. Adding context with .context()
.context("message") wraps an error with a description of what you were trying to do. This is the single most useful feature of anyhow: errors explain themselves.
use anyhow::{Context, Result};
fn read_port(raw: &str) -> Result<u16> {
raw.trim().parse::<u16>().context("port must be a number between 0 and 65535")
}
fn start_server(config_line: &str) -> Result<String> {
let value = config_line.strip_prefix("port=").context("config line must start with 'port='")?;
let port = read_port(value).context("reading server config")?;
Ok(format!("listening on {port}"))
}
fn main() {
for line in ["port=8080", "port=99999", "host=x"] {
match start_server(line) {
Ok(msg) => println!("{msg}"),
Err(e) => println!("error: {e:#}"),
}
}
}listening on 8080
error: reading server config: port must be a number between 0 and 65535: number too large to fit in target type
error: config line must start with 'port='.context() also works on Option, turning None into an error with that message. {:#} prints the whole chain on one line, separated by colons.
10. .with_context() for messages built at runtime
with_context(|| ...) builds the message only if there is an error, which avoids formatting strings on the happy path.
use anyhow::{Context, Result};
fn load_user(id: u32, db: &[(u32, &str)]) -> Result<String> {
let name = db.iter().find(|(i, _)| *i == id).map(|(_, n)| n.to_string()).with_context(|| format!("user {id} does not exist"))?;
Ok(name)
}
fn load_team(ids: &[u32], db: &[(u32, &str)]) -> Result<Vec<String>> {
ids.iter().map(|&id| load_user(id, db).with_context(|| format!("loading team member #{id}"))).collect()
}
fn main() {
let db = [(1, "ana"), (2, "ben")];
println!("{:?}", load_team(&[1, 2], &db));
println!("{:#}", load_team(&[1, 7, 2], &db).unwrap_err());
}Ok(["ana", "ben"])
loading team member #7: user 7 does not exist11. bail!: return an error immediately
bail!("...") is shorthand for return Err(anyhow!("...")).
use anyhow::{bail, Result};
fn transfer(balance: i64, amount: i64) -> Result<i64> {
if amount <= 0 {
bail!("transfer amount must be positive, got {amount}");
}
if amount > balance {
bail!("insufficient funds: balance {balance}, requested {amount}");
}
Ok(balance - amount)
}
fn main() {
for amount in [50, -5, 500] {
match transfer(100, amount) {
Ok(left) => println!("ok, {left} left"),
Err(e) => println!("error: {e}"),
}
}
}ok, 50 left
error: transfer amount must be positive, got -5
error: insufficient funds: balance 100, requested 50012. ensure!: check a condition or fail
ensure!(condition, "message") returns an error when the condition is false, like Guava's Preconditions.checkArgument.
use anyhow::{ensure, Result};
fn create_account(username: &str, age: u32) -> Result<String> {
ensure!(username.len() >= 3, "username '{username}' is too short");
ensure!(username.chars().all(|c| c.is_ascii_alphanumeric()), "username may only contain letters and digits");
ensure!(age >= 13, "users must be at least 13 years old");
Ok(format!("created {username}"))
}
fn main() {
for (name, age) in [("ferris", 8), ("al", 30), ("bad name", 30), ("ferris", 21)] {
println!("{:?}", create_account(name, age).map_err(|e| e.to_string()));
}
}Err("users must be at least 13 years old")
Err("username 'al' is too short")
Err("username may only contain letters and digits")
Ok("created ferris")13. Three ways to print an anyhow::Error
{}prints only the outermost message.{:#}prints the whole chain on one line.{:?}prints the outermost message and a "Caused by:" list, the format used whenmainreturns an error.
use anyhow::{Context, Result};
fn parse_config() -> Result<u32> {
"12x".parse::<u32>().context("parsing 'workers'").context("loading app.toml")
}
fn main() {
let e = parse_config().unwrap_err();
println!("{{}}: {e}");
println!("{{:#}}: {e:#}");
println!("{{:?}}:\n{e:?}");
}{}: loading app.toml
{:#}: loading app.toml: parsing 'workers': invalid digit found in string
{:?}:
loading app.toml
Caused by:
0: parsing 'workers'
1: invalid digit found in string14. main returning anyhow::Result
Returning an error from main prints it with the "Caused by:" list and exits with code 1.
use anyhow::{Context, Result};
fn read_threshold(raw: &str) -> Result<f64> {
raw.parse().with_context(|| format!("threshold '{raw}' is not a number"))
}
fn main() -> Result<()> {
let t = read_threshold("0.75")?;
println!("threshold {t}");
let t = read_threshold("high").context("loading alert rules")?;
println!("threshold {t}");
Ok(())
}threshold 0.75
Error: loading alert rules
Caused by:
0: threshold 'high' is not a number
1: invalid float literal15. Walking the chain
err.chain() iterates over every error in the chain, from outermost to root cause. err.root_cause() jumps to the end.
use anyhow::{Context, Result};
fn step3() -> Result<i32> {
Ok("seven".parse::<i32>()?)
}
fn step2() -> Result<i32> {
step3().context("reading retry count")
}
fn step1() -> Result<i32> {
step2().context("starting worker pool")
}
fn main() {
let e = step1().unwrap_err();
for (depth, cause) in e.chain().enumerate() {
println!("{depth}: {cause}");
}
println!("root cause: {}", e.root_cause());
}0: starting worker pool
1: reading retry count
2: invalid digit found in string
root cause: invalid digit found in string16. Downcasting an anyhow::Error
An anyhow::Error still remembers the concrete errors inside it. downcast_ref::<T>() finds them, so an application can react to a specific library error.
use anyhow::{Context, Result};
use thiserror::Error;
#[derive(Debug, Error)]
enum StorageError {
#[error("object {0} not found")]
NotFound(String),
#[error("storage quota exceeded")]
QuotaExceeded,
}
fn get(key: &str) -> Result<String, StorageError> {
match key {
"logo.png" => Ok(String::from("PNG...")),
"huge.bin" => Err(StorageError::QuotaExceeded),
_ => Err(StorageError::NotFound(key.to_string())),
}
}
fn handler(key: &str) -> Result<String> {
get(key).with_context(|| format!("serving /files/{key}"))
}
fn main() {
for key in ["logo.png", "missing.txt", "huge.bin"] {
let status = match handler(key) {
Ok(_) => 200,
Err(e) => match e.downcast_ref::<StorageError>() {
Some(StorageError::NotFound(_)) => 404,
Some(StorageError::QuotaExceeded) => 507,
None => 500,
},
};
println!("{key}: {status}");
}
}logo.png: 200
missing.txt: 404
huge.bin: 507downcast_ref looks through context layers to find the original error.
17. anyhow! to create ad-hoc errors
anyhow!("...") creates an anyhow::Error from a message, useful in map_err, ok_or_else, and match arms.
use anyhow::{anyhow, Result};
fn parse_duration(s: &str) -> Result<u64> {
let (num, unit) = s.split_at(s.find(|c: char| c.is_alphabetic()).ok_or_else(|| anyhow!("'{s}' has no unit"))?);
let n: u64 = num.parse().map_err(|_| anyhow!("'{s}' does not start with a number"))?;
match unit {
"s" => Ok(n),
"m" => Ok(n * 60),
"h" => Ok(n * 3_600),
other => Err(anyhow!("unknown unit '{other}' in '{s}'")),
}
}
fn main() {
for s in ["30s", "5m", "2h", "10", "xm", "3d"] {
println!("{s:>3} -> {:?}", parse_duration(s).map_err(|e| e.to_string()));
}
}30s -> Ok(30)
5m -> Ok(300)
2h -> Ok(7200)
10 -> Err("'10' has no unit")
xm -> Err("'xm' does not start with a number")
3d -> Err("unknown unit 'd' in '3d'")Part 3: Using them together
18. thiserror in the library, anyhow in the application
The library layer exposes a precise error enum. The application wraps it with context, and only matches on it where needed.
mod billing {
use thiserror::Error;
#[derive(Debug, Error)]
pub enum BillingError {
#[error("customer {0} has no payment method")]
NoPaymentMethod(u32),
#[error("amount {0} exceeds the single-charge limit")]
OverLimit(u64),
}
pub fn charge(customer: u32, cents: u64) -> Result<String, BillingError> {
if customer == 2 {
return Err(BillingError::NoPaymentMethod(customer));
}
if cents > 1_000_000 {
return Err(BillingError::OverLimit(cents));
}
Ok(format!("inv-{customer}-{cents}"))
}
}
use anyhow::{Context, Result};
fn renew_subscription(customer: u32, cents: u64) -> Result<String> {
let invoice = billing::charge(customer, cents).with_context(|| format!("renewing subscription for customer {customer}"))?;
Ok(invoice)
}
fn main() {
for (customer, cents) in [(1, 1_999), (2, 1_999), (3, 5_000_000)] {
match renew_subscription(customer, cents) {
Ok(inv) => println!("renewed: {inv}"),
Err(e) => println!("failed: {e:#}"),
}
}
}renewed: inv-1-1999
failed: renewing subscription for customer 2: customer 2 has no payment method
failed: renewing subscription for customer 3: amount 5000000 exceeds the single-charge limit19. Realistic example: a configuration loader with anyhow
use anyhow::{bail, ensure, Context, Result};
#[derive(Debug)]
struct Config {
port: u16,
workers: usize,
database_url: String,
}
fn value<'a>(text: &'a str, key: &str) -> Option<&'a str> {
text.lines().filter_map(|l| l.split_once('=')).find(|(k, _)| k.trim() == key).map(|(_, v)| v.trim())
}
fn load(text: &str) -> Result<Config> {
let port = value(text, "port").context("missing 'port'")?.parse().context("invalid 'port'")?;
let workers: usize = value(text, "workers").unwrap_or("4").parse().context("invalid 'workers'")?;
ensure!((1..=64).contains(&workers), "'workers' must be between 1 and 64, got {workers}");
let database_url = value(text, "database_url").context("missing 'database_url'")?.to_string();
if !database_url.starts_with("postgres://") {
bail!("only postgres is supported, got '{database_url}'");
}
Ok(Config { port, workers, database_url })
}
fn main() {
let files = [
("good.conf", "port=8080\nworkers=8\ndatabase_url=postgres://db/app"),
("no-port.conf", "database_url=postgres://db/app"),
("bad-workers.conf", "port=80\nworkers=500\ndatabase_url=postgres://db/app"),
("mysql.conf", "port=80\ndatabase_url=mysql://db/app"),
];
for (name, text) in files {
match load(text).with_context(|| format!("while loading {name}")) {
Ok(c) => println!("{:?}", c),
Err(e) => println!("error: {e:#}"),
}
}
}Config { port: 8080, workers: 8, database_url: "postgres://db/app" }
error: while loading no-port.conf: missing 'port'
error: while loading bad-workers.conf: 'workers' must be between 1 and 64, got 500
error: while loading mysql.conf: only postgres is supported, got 'mysql://db/app'20. Realistic example: a service layer with thiserror
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RepoError {
#[error("row {0} not found")]
NotFound(u64),
#[error("database connection lost")]
Disconnected,
}
#[derive(Debug, Error)]
pub enum UserServiceError {
#[error("user {0} does not exist")]
NoSuchUser(u64),
#[error("{0} is already taken")]
EmailTaken(String),
#[error("storage error")]
Storage(#[from] RepoError),
}
struct Repo {
users: Vec<(u64, String)>,
online: bool,
}
impl Repo {
fn find(&self, id: u64) -> Result<&str, RepoError> {
if !self.online {
return Err(RepoError::Disconnected);
}
self.users.iter().find(|(i, _)| *i == id).map(|(_, e)| e.as_str()).ok_or(RepoError::NotFound(id))
}
}
fn change_email(repo: &mut Repo, id: u64, new: &str) -> Result<String, UserServiceError> {
if repo.users.iter().any(|(_, e)| e == new) {
return Err(UserServiceError::EmailTaken(new.to_string()));
}
let old = match repo.find(id) {
Ok(e) => e.to_string(),
Err(RepoError::NotFound(id)) => return Err(UserServiceError::NoSuchUser(id)),
Err(other) => return Err(other.into()),
};
if let Some(u) = repo.users.iter_mut().find(|(i, _)| *i == id) {
u.1 = new.to_string();
}
Ok(format!("{old} -> {new}"))
}
fn main() {
use std::error::Error;
let mut repo = Repo { users: vec![(1, String::from("ana@x.com")), (2, String::from("ben@x.com"))], online: true };
println!("{:?}", change_email(&mut repo, 1, "ana@new.com").map_err(|e| e.to_string()));
println!("{:?}", change_email(&mut repo, 1, "ben@x.com").map_err(|e| e.to_string()));
println!("{:?}", change_email(&mut repo, 9, "z@x.com").map_err(|e| e.to_string()));
repo.online = false;
let e = change_email(&mut repo, 2, "b@x.com").unwrap_err();
println!("{e} (caused by: {})", e.source().unwrap());
}Ok("ana@x.com -> ana@new.com")
Err("ben@x.com is already taken")
Err("user 9 does not exist")
storage error (caused by: database connection lost)The service translates RepoError::NotFound into its own NoSuchUser, but lets other storage errors pass through as Storage, keeping the original as the source.
21. Realistic example: HTTP errors with status codes
A thiserror enum plus a status() method gives one place that decides both the message and the status code of every failure.
use thiserror::Error;
#[derive(Debug, Error)]
enum ApiError {
#[error("{resource} {id} not found")]
NotFound { resource: &'static str, id: u64 },
#[error("invalid request: {0}")]
BadRequest(String),
#[error("you do not have access to {0}")]
Forbidden(String),
#[error("internal error")]
Internal(#[from] std::num::ParseIntError),
}
impl ApiError {
fn status(&self) -> u16 {
match self {
ApiError::NotFound { .. } => 404,
ApiError::BadRequest(_) => 400,
ApiError::Forbidden(_) => 403,
ApiError::Internal(_) => 500,
}
}
}
fn get_order(user: &str, raw_id: &str) -> Result<String, ApiError> {
if raw_id.is_empty() {
return Err(ApiError::BadRequest(String::from("missing order id")));
}
let id: u64 = raw_id.parse()?;
match (user, id) {
(_, 404) => Err(ApiError::NotFound { resource: "order", id }),
("guest", _) => Err(ApiError::Forbidden(format!("order {id}"))),
_ => Ok(format!("{{\"order\": {id}}}")),
}
}
fn main() {
for (user, id) in [("ana", "7"), ("ana", "404"), ("guest", "7"), ("ana", ""), ("ana", "7x")] {
match get_order(user, id) {
Ok(body) => println!("200 {body}"),
Err(e) => println!("{} {e}", e.status()),
}
}
}200 {"order": 7}
404 order 404 not found
403 you do not have access to order 7
400 invalid request: missing order id
500 internal error22. Realistic example: a CLI tool's main with anyhow
The whole program reports every failure with full context, and exits with a non-zero code on error. This is how most Rust command-line tools are structured.
use anyhow::{bail, Context, Result};
fn parse_line(line: &str) -> Result<(String, f64)> {
let (item, price) = line.split_once(':').context("expected 'item: price'")?;
let price: f64 = price.trim().parse().with_context(|| format!("price '{}' is not a number", price.trim()))?;
if price < 0.0 {
bail!("price cannot be negative");
}
Ok((item.trim().to_string(), price))
}
fn total(receipt: &str) -> Result<f64> {
let mut sum = 0.0;
for (n, line) in receipt.lines().enumerate() {
let (_, price) = parse_line(line).with_context(|| format!("line {}: '{line}'", n + 1))?;
sum += price;
}
Ok(sum)
}
fn main() -> Result<()> {
println!("total {:.2}", total("coffee: 3.50\nbagel: 2.25").context("processing receipt-1")?);
println!("total {:.2}", total("coffee: 3.50\nbagel two twenty-five").context("processing receipt-2")?);
Ok(())
}total 5.75
Error: processing receipt-2
Caused by:
0: line 2: 'bagel two twenty-five'
1: expected 'item: price'Summary
thiserror:#[derive(Error)]plus#[error("...")]messages,#[from]for automatic?conversions,#[source]for causes, and#[error(transparent)]for pass-through variants. The result is an ordinary error enum callers can match.anyhow:anyhow::Result<T>accepts any error through?. Add meaning with.context()and.with_context(), fail early withbail!andensure!, and create errors withanyhow!.- Print
anyhowerrors with{}(outermost),{:#}(whole chain on one line), or{:?}(with a "Caused by:" list). downcast_refrecovers specific errors from ananyhow::Error, even under context layers.- Rule of thumb: libraries define precise errors with
thiserror; applications report them withanyhow.
Next: Error Handling Patterns.
