Skip to content

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

@Frando

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions