--- name: rust-error-handling description: When to use `anyhow` vs `thiserror`, how to add context, when to panic. Rust 1.97+ idioms. when_to_use: Any time you write a fn returning `Result` in Rust. tags: [rust, errors] --- # Rust error handling ## Which error type - **Application binary** (`clawmates-server`, `clawmates-node`) → `anyhow::Result` everywhere. You never match on the error at runtime; you print + log + exit. - **Library crate** (`cm-db`, `cm-api`, `cm-runtime`) → `thiserror` on a per-module `Error` enum. Callers get typed variants they can match. - **Test code** → `anyhow` is fine even in library crates. Tests don't match on error types. ## `thiserror` shape ```rust use thiserror::Error; #[derive(Debug, Error)] pub enum DbError { #[error("row not found")] NotFound, #[error("workspace mismatch: expected {expected}, got {actual}")] WorkspaceMismatch { expected: Uuid, actual: Uuid }, #[error(transparent)] Sqlx(#[from] sqlx::Error), } ``` Rules: - **`#[from]` for exactly one wrapped external type per variant.** Otherwise `?` becomes ambiguous. - **`#[error("...")]` includes the values.** Never `#[error("db error")]` — that's information-lossy. - **`#[error(transparent)]`** for pass-through wrappers where the inner error's message is already sufficient. ## Adding context Every `?` at a module boundary needs context. Without it, a deeply-nested `sqlx::Error` is unattributable. ```rust use anyhow::Context; let user = load(pool, id) .await .with_context(|| format!("load user {id}"))?; ``` - **`.context("...")`** for a static string. - **`.with_context(|| ...)`** when the message includes runtime data (allocation is lazy — only fires on error). ## When to panic Panic is fine when: - The invariant CAN'T be violated at runtime (`unreachable!("mission_kind is validated at insert time")`). - You're in `main()` before the async runtime, and the config file is broken — `.expect("clawmates.toml is required")`. - Test-only preconditions (`assert_eq!` inside `#[test]`). Panic is NOT fine when: - The input came from the network. - The input came from a config parse. - The input came from a database row that a migration is supposed to guarantee — write an `Error` variant explaining WHY the row shape violates the invariant. ## Backtrace hygiene - Set `RUST_BACKTRACE=1` in dev + CI. Off in prod (leaks internals). - `anyhow::Error` captures a backtrace automatically. Log it via `error!("op failed: {e:?}")` — the `?` formatter includes the chain + backtrace.