Skip to content

Migrating from snafu/n0-snafu to n0-error with AI help #13

Description

@Frando

I had ChatGPT create a migration guide for migrating crates from snafu/n0-snafu to n0-error by looking at the diff of

With this prompt ChatGPT performed quite well in migrating other crates. Leaving it here so that it can be reused.

Snafu → n0-error Migration Guide

Audience: an AI coding agent converting a Rust workspace from Snafu/n0-snafu to n0-error.
Scope: update error types, call sites, and ergonomics while preserving call-site location and readable error reports.

Key Concepts
- StackError: n0-error’s trait implemented by errors created via the `#[stack_error(..)]` macro. These carry structured messages and can preserve call-site metadata (when `RUST_BACKTRACE=1` or `RUST_ERROR_LOCATION=1`).
- AnyError: a type-erased error similar to anyhow::Error, which preserves StackError location if present and can capture call-site location for std errors when constructed via `from_std`.
- Context vs std_context: Use `ResultExt` variants correctly to preserve location and intent.
  - Use `.context("msg")` and `.with_context(..)` on results whose error is a StackError (i.e., derived with `#[stack_error(..)]`, or Option’s `NoneError`).
  - Use `.std_context("msg")` and `.with_std_context(..)` only when the error does not carry StackError call-site metadata (typical for external std/third-party errors). This captures the call-site into AnyError.
  - Never convert a StackError result to AnyError via `anyerr`/`std_context`; prefer `.context(..)` or `?` to keep metadata.
- anyerr: only for cases where you explicitly need to create an AnyError (message-only, or you must wrap a non-StackError error value). Never use `anyerr(..)` on results that already have StackErrors.

What Changed (HEAD vs origin/main)
- Error enums/structs move from `#[derive(Snafu)]` to `#[stack_error(derive, add_meta, ..)]`.
- Snafu attributes map to n0-error attributes:
  - `#[snafu(display(".."))]` → `#[error("..")].`
  - `#[snafu(transparent)]` → `#[error(transparent)].`
  - Fields that are std/3rd-party errors: add `#[error(std_err)]` so the macro knows they’re not StackErrors.
  - Use `from_sources` / `std_sources` on the type when you want `From<..>` for source variants.
- Backtrace/span-trace fields are removed; `add_meta` collects call-site location for StackErrors (opt-in via env at runtime).
- Call sites change from `snafu::ResultExt` to n0-error extensions:
  - For std/3rd-party errors returning AnyError in app/tests: use `.std_context("msg")` (or `.anyerr()` when context is not needed).
  - For typed StackError flows: use `.context("msg")` (or map to a typed variant via `e!(..)` if you’re converting types).
- Snafu macros replaced:
  - `snafu::ensure!` → `n0_error::ensure!` (typed error) or `n0_error::ensure_any!` (message-only AnyError).
  - `whatever!` → `bail_any!(..)` (or `anyerr!(..)` if you need an error value instead of returning).
  - Early-return typed error: `bail!(MyError::Variant { .. }[, source])`.
  - Unwrap with conversion: `try_or!(result, MyError::Variant { .. })` and `try_or_any!(result, "ctx")`.

Step-by-Step Migration

1) Cargo.toml and imports
- Remove Snafu and n0-snafu deps. Add n0-error:
  - Workspace local path example: `n0-error = { path = "../../n0-error" }` (adjust per crate).
- Imports in code:
  - Replace `use snafu::{Snafu, ResultExt, OptionExt};` with `use n0_error::{stack_error, StackResultExt, StdResultExt};` plus macros as needed: `use n0_error::{e, ensure};`.
  - In tests or apps that return AnyError, also `use n0_error::Result;` for the alias.

2) Error types (enums/structs)
- Replace `#[derive(Snafu)]` with the macro attribute form and add location support:
  - Before:
    - `#[derive(Debug, Snafu)]`
    - Per-variant `#[snafu(display(".."))]`, `#[snafu(transparent)]`.
    - Often explicit `backtrace: Option<Backtrace>`, `span_trace: n0_snafu::SpanTrace`.
  - After:
    - `#[stack_error(derive, add_meta)]` on the type.
    - Rename attributes: `#[error("..")]` and `#[error(transparent)]`.
    - For std/3rd-party source fields add `#[error(std_err)]` and keep the field named `source`.
    - If you want `From<Source>` for your transparent/source variants, add:
      - `#[stack_error(derive, add_meta, from_sources)]` for StackError sources.
      - `#[stack_error(derive, add_meta, std_sources)]` for std/third-party sources.
    - Remove Snafu backtrace/span-trace fields; `add_meta` handles capture under `RUST_BACKTRACE=1` or `RUST_ERROR_LOCATION=1`.

Examples
- Transparent std/3rd-party source:
  - Before (Snafu):
    - `#[snafu(transparent)] Io { source: std::io::Error, .. }`
  - After (n0-error):
    - `#[error(transparent)] Io { #[error(std_err)] source: std::io::Error }`

- Simple message variant:
  - Before: `#[snafu(display("TLS[manual] timeout"))] Timeout { .. }`
  - After: `#[error("TLS[manual] timeout")] Timeout {}`

3) Constructing typed errors
- Use `e!` to construct errors without manually filling the `meta` field:
  - No source: `e!(MyError::Variant)`.
  - With source: `e!(MyError::Variant, err)`.
  - With fields: `e!(MyError::Variant { path: p }, err)` or `e!(MyError::Variant { foo })`.
- Early return a typed error: `bail!(MyError::Variant { .. }[, source])`.
- As a rule, prefer typed errors in libraries; use AnyError primarily in app/test layers or when bubbling through mixed external APIs.

4) Result extensions: context vs std_context
- If the error is a StackError you control (derived with `#[stack_error(..)]`, or `Option`’s `NoneError`):
  - Use `.context("msg")` or `.with_context(|e| format!(..))` to convert to AnyError with added context while preserving the StackError’s call-site metadata.
- Prefer just `?` when no extra message is needed; StackError → AnyError conversion is automatic and preserves metadata.
- If the error is a std/3rd-party error (e.g., `std::io::Error`, `hyper::Error`, `rcgen::Error`, `tokio::task::JoinError`):
  - Use `.std_context("msg")` or `.with_std_context(..)`. This converts to AnyError and captures the call-site location.
  - If you don’t need context, and you are already in a function returning `Result<_, AnyError>`, you can use `.anyerr()`.
- Important: Do not call `anyerr()`/`std_context()` on results that already use StackError (your derived types) — use `.context(..)` or `?` instead to keep location metadata.

5) Mapping/Converting errors
- Snafu’s `.context(ErrorVariant)` patterns that produced typed errors map to either:
  - Using `e!` inside `map_err`: `x.map_err(|err| e!(MyError::Variant { field }, err))?;`, or
  - Adding `from_sources`/`std_sources` to the error type and using `?` with `Into`: `x.map_err(Into::into)?;` if a `From<Source>` is generated.
- When a call returns std error but you want a typed error with that std error as a source:
  - `foo().map_err(|err| e!(MyError::Io, err))?;`
  - If you don’t need a typed error at this layer and return AnyError, prefer `foo().std_context("msg")?;` for simplicity.

6) Assertions and early returns
- `snafu::ensure!(predicate, ...)` → `n0_error::ensure!(predicate, MyError::Variant { .. })` for typed flows.
- Pure message-only early return: `ensure_any!(predicate, "message with {vars}")` or `bail_any!("message")`.
- Replace `whatever!(..)` with `bail_any!(..)` (or `anyerr!(..)` when you need to produce an `AnyError` value without returning).
- Unwrapping helpers:
  - `try_or!(res, MyError::Variant { .. })` returns early with a typed error using the source from `res`.
  - `try_or_any!(res, "ctx")` returns early with an AnyError adding context.

7) Options and None handling
- Option is supported directly by n0-error’s extensions:
  - `.context("msg")` preserves location via an internal `NoneError` StackError.
  - `.std_context("msg")` is also available but prefer `.context(..)` for Option flows.
  - Alternative: `ok_or_else(|| e!(MyError::MissingThing { .. }))?;` when mapping to your typed errors.

8) Tests, examples, apps returning AnyError
- Update function signatures to `n0_error::Result<T>` where appropriate.
- Use `.std_context("msg")` on std/3rd-party errors to capture call-site for better debug output, similar to the diffs:
  - `quinn::Endpoint::client(..).std_context("client")?;`
  - `server_task.await.std_context("join")??;`
- Add `use n0_error::{Result, StdResultExt};` in tests for convenience.

Attribute/Cookbook Mappings
- Per-variant mappings:
  - Snafu display → `#[error("…")]`.
  - Snafu transparent → `#[error(transparent)]` with `#[error(std_err)]` on source if it’s std/3rd-party.
  - Snafu custom fields for backtrace/spantrace → remove; `add_meta` handles location.
- Type-level flags:
  - `#[stack_error(derive, add_meta)]` is the default base.
  - Add `from_sources` to auto-derive `From<StackErrorSource>` for variants with `source: T` where `T: StackError`.
  - Add `std_sources` to auto-derive `From<StdSource>` for variants with `#[error(std_err)] source: E`.
- Source field guidelines:
  - Name the source field `source`.
  - For StackError sources (other derived errors), no extra attribute is needed.
  - For std/3rd-party errors, add `#[error(std_err)]` and consider `std_sources` at type level if you want `From<E>`.

Common Transform Recipes
- Context messages on std errors (apps/tests):
  - Before: `foo().context("client")?;`
  - After: `foo().std_context("client")?;`
- Context messages on StackErrors:
  - Before: `my_api().context("failed to do X")?;`
  - After: `my_api().context("failed to do X")?;` (same call but now `my_api()` returns a StackError type).
- Typed conversion with source:
  - Before: `foo().context(MyError::VariantSnafu)?;`
  - After: `foo().map_err(|err| e!(MyError::Variant { .. }, err))?;` or add `from_sources` and use `?` with `Into`.
- Early return message-only:
  - Before: `whatever!("cannot get ipv4 addr {addr:?}");`
  - After: `bail_any!("cannot get ipv4 addr {addr:?}");`
- Ensure:
  - Before: `snafu::ensure!(cond, MyError { .. });`
  - After: `n0_error::ensure!(cond, MyError::Variant { .. });`

Do/Don’t Checklist
- Do: Prefer typed StackErrors in library crates. Use AnyError at boundaries (examples, tests, bin crates) or when crossing many external APIs.
- Do: Use `.context(..)` for StackError results; it preserves metadata. Use `.std_context(..)` for std/3rd-party errors.
- Do: Use `e!` to build typed errors; `bail!`/`bail_any!` to return early.
- Don’t: Call `anyerr()` on results that already carry StackErrors (including Options). Use `.context(..)` or `?` instead.
- Don’t: Keep Snafu backtrace/spantrace; remove them and rely on `add_meta` + `RUST_BACKTRACE=1`.
- Don’t: Use `map_err(|_| …)` when you need the original error as source; use `map_err(|err| e!(.., err))`.

Validation Commands
- Build and tests:
  - `cargo check --workspace`
  - `cargo nextest run` or `cargo test --workspace`
- Lints/format:
  - `cargo clippy --workspace --all-features -D warnings`
  - `cargo fmt --all`
- Audit for leftovers:
  - `rg -n "\b(snafu|n0_snafu|Snafu|whatever!|ResultExt|OptionExt)\b" -S -g '!target/'`
  - `cargo tree -p <crate> | rg snafu`

Rationale for context vs std_context and anyerr
- StackError results: When you `.context(..)` or use `?`, the AnyError retains the inner StackError’s call-site metadata. This is optimal for derived errors and Option’s `NoneError`.
- std/3rd-party errors: Using `.std_context(..)` or `.anyerr()` captures the current call-site into AnyError, giving you useful locations in debug output. Converting such errors via the StackError route would not capture the call-site.
- `anyerr(..)`: create an AnyError when there is no typed error to construct or you’re emitting a message-only error. Avoid it for StackError results.

Edge Cases and Tips
- Transparent pass-through: If a variant should fully forward the source’s message, mark it `#[error(transparent)]`.
- Multiple source kinds: Prefer adding `from_sources` and `std_sources` at the type level rather than per-field `from` when many variants wrap sources.
- Options: Prefer `.context("msg")` to leverage `NoneError`; allows consistent location capture and formatting.
- Interop with anyhow: n0-error has an optional feature to/from anyhow. Prefer staying in AnyError/StackError to keep location semantics consistent.

Quick Porting Flow (per file)
- Replace Snafu derives/attrs as above.
- Delete backtrace/span-trace fields.
- Update imports/macros: `stack_error`, `e!`, `ensure!`, `bail!`, `bail_any!`, `StackResultExt`, `StdResultExt`.
- Convert `.context(..)` usages:
  - std/third-party → `.std_context(..)` (or `anyerr()` if no message needed).
  - StackError → `.context(..)` or propagate with `?`.
- Convert `OptionExt` flows to `.context(..)` or `ok_or_else(e!(..))?`.
- When mapping to typed errors, use `e!(.., source)` inside `map_err`.
- Re-run checks and adjust `from_sources/std_sources` on types to clean up `map_err(Into::into)` opportunities.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions