-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
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
Labels
No labels