Expand description
Ergonomic, structured error handling for Rust.
oopsie centers on a single attribute macro:
#[oopsie] — generates context selectors, Display, Debug, and Error
impls for your error type. Pass traced to also capture a backtrace and, with the
tracing feature, a span-trace.
§Quick start
Define your error type:
#[oopsie::oopsie]
pub enum AppError {
#[oopsie("Connection to {host} failed")]
Connect { host: String, source: std::io::Error },
#[oopsie("Key not found: {key}")]
MissingKey { key: String },
}Use it — bring the extension traits into scope with the prelude:
use oopsie::prelude::*;
fn connect(host: &str) -> Result<(), AppError> {
std::net::TcpStream::connect(host)
.context(app_oopsies::Connect { host })?;
Ok(())
}§Diagnostics
Pass traced to automatically capture a backtrace field and, with the
tracing feature, a span-trace:
#[oopsie::oopsie(traced)]
pub enum AppError {
#[oopsie("Connection to {host} failed")]
Connect { host: String, source: std::io::Error },
}traced also assigns each variant an automatic error code —
module_path::Type::Variant — shown as Error[...] in Report headers.
Override it per variant with #[oopsie(code = "...")], or disable it with
#[oopsie::oopsie(traced, code = false)].
Call start_marker! early in a thread to hide the setup frames below it
from rendered traces.
See the #[oopsie] documentation for the full parameter reference.
§Reporting
Report renders an error — message, source chain, span trace, backtrace —
as a rich, colorized report. It implements Termination,
so it can be returned straight from main. Report::run additionally
installs the library’s panic hook for the duration of the closure, so
panics are rendered in the same style:
use oopsie::Report;
use oopsie::prelude::*;
#[oopsie::oopsie(traced)]
pub enum AppError {
#[oopsie("Key not found: {key}")]
MissingKey { key: String },
}
fn run() -> Result<(), AppError> {
let config: Option<&str> = Some("42");
let _value = config.context(app_oopsies::MissingKey { key: "answer" })?;
Ok(())
}
fn main() -> Report<AppError> {
Report::run(run)
}When run returns an error, the report is printed to stderr and the
process exits with a failure code. To render panics outside of
Report::run, install the hook process-wide with install_panic_hook
once, early in main. Color output is auto-detected; override it with
set_color_mode or per report via Report::no_colors /
Report::force_colors.
(Requires the fancy feature.)
§Beyond typed errors
Welpis a string-shaped escape hatch for prototypes and one-off errors:Welp::new("..."), or.welp_context("...")on anyResultvia the prelude.- The
oopsie::erasedmodule converts any error into a serializable, type-erased representation — message, source chain, code/help, span trace, and backtrace — for transporting errors across process boundaries, e.g. API error responses. (Requires theserdefeature.)
§What gets generated
For each variant or struct, #[oopsie] generates a context selector — a struct
containing all the fields except the source error and any #[oopsie(capture)] fields.
Selectors for leaf variants (no source) expose .build() and .fail(),
and work with Option::context. Selectors for variants with a source
expose .build_error(source) via the Contextual trait.
The methods are mutually exclusive — a source selector has no .build().
All selector fields accept Into<T>, so you can pass "str" for a String field.
.context(selector) on Result / Option builds the error for you —
you only need to call these methods directly when constructing errors manually.
§Selector naming
The selector name is the variant name (for enums) or struct name (for structs),
with a trailing "Error" suffix stripped. Enums and structs share this rule.
Append a suffix with #[oopsie(suffix)] ("Oopsie") or #[oopsie(suffix = "X")];
the default is none.
| Variant / struct | Selector name |
|---|---|
Connect (variant) | Connect |
ConnectionError (variant) | Connection |
QueryError (struct) | Query |
§Module wrapping
Selectors are placed in a generated module by default, for both enums and
structs. The auto-generated module name is derived from the error type name:
strip trailing "Error", convert to snake_case, append _oopsies:
| Error type | Module |
|---|---|
AppError | app_oopsies |
ConnError | conn_oopsies |
MyError | my_oopsies |
Control this with #[oopsie(module(false))] (disable) or #[oopsie(module(custom_name))].
An error type declared inside a function body requires #[oopsie(module(false))]:
the generated module cannot reference items local to a function, so the wrapped
selectors would be unable to name the error type.
§Display messages
Display strings follow format! semantics with named or positional interpolation:
#[oopsie("Failed to read {path}")]— named field#[oopsie("Got {} errors", count)]— positional
If no display attribute is given, the variant or struct name is used verbatim as the message.
§Attribute reference
Each keyword has its own subsection below; the per-scope tables are a scannable index into them.
§Container (enum / struct)
| Keyword | Effect |
|---|---|
module | Wrap selectors in a module |
suffix | Suffix on selector names |
size | Compile-time size assertion |
path | Path to the oopsie crate |
vis | Selector visibility |
exit_code | Default process exit code |
The short form #[oopsie("msg")] on a container sets the struct’s display
message (on enums the message goes on each variant instead).
Wraps the generated context selectors in a module, keeping them out of the
surrounding namespace. Enabled by default for both enums and structs (auto-named
by stripping a trailing Error, converting to snake_case, and appending
_oopsies).
Forms: module, module(true), module(false), module(name), module = "name".
An error type declared inside a function body needs module(false): the
generated module cannot reference items local to a function, so wrapping the
selectors in one leaves them unable to name the error type.
§Example
#[oopsie::oopsie]
#[oopsie(module(my_errors))]
pub enum AppError {
#[oopsie("not found: {key}")]
Missing { key: String },
}
fn main() {
let err = my_errors::Missing { key: "k".to_owned() }.build();
assert_eq!(err.to_string(), "not found: k");
}Appends a suffix to generated selector names. The default is none for both enums
and structs — the selector base is the variant or struct name with a trailing
Error stripped. A suffix is mainly useful when the stripped name would clash
with the surrounding scope (e.g. a non-Error-suffixed struct under
module(false), whose selector would otherwise share the type’s name).
Forms: suffix, suffix(true), suffix(false), suffix("X"), suffix = "X".
The flag forms (suffix/suffix(true)) use "Oopsie".
§Example
#[oopsie::oopsie]
#[oopsie(suffix("Ctx"))]
pub struct QueryError {
query: String,
}
fn main() {
let err = query_oopsies::QueryCtx { query: "SELECT 1".to_owned() }.build();
assert_eq!(err.to_string(), "QueryError");
}Asserts the size of the error type at compile time, so an accidental growth of
the error fails the build instead of silently bloating every Result that
carries it. Accepts an exact size or a range, in bytes.
Forms: size(N), size(..=N), size(N..), size(N..=M).
Not available on a generic error type: its size depends on the type arguments
and has no single value to assert, so combining size(...) with generic
parameters is rejected.
§Example
#[oopsie::oopsie]
#[oopsie(size(..=64))]
pub enum AppError {
#[oopsie("not found: {key}")]
Missing { key: String },
}Overrides the path to the oopsie crate used in the generated code. Needed when
oopsie is re-exported under a different path or renamed in Cargo.toml.
Defaults to ::oopsie.
Forms: path = "some::path", path = some::path.
§Example
// A real custom path must name a crate that actually re-exports `oopsie`,
// which can't exist inside this crate's own doctest.
#[oopsie::oopsie]
#[oopsie(path = "my_crate::reexports::oopsie")]
pub enum AppError {
#[oopsie("boom")]
Boom,
}Overrides the visibility of the generated selectors (and their module, if any).
Defaults to the error type’s own visibility. A variant-level vis(...) overrides
this for that one variant’s selector.
Forms: vis(pub), vis(pub(crate)), vis = "pub(crate)".
§Example
#[oopsie::oopsie]
#[oopsie(module(errs), vis(pub(crate)))]
pub enum AppError {
#[oopsie("not found: {key}")]
Missing { key: String },
}
fn main() {
let err = errs::Missing { key: "k".to_owned() }.build();
assert_eq!(err.to_string(), "not found: k");
}Sets the default process exit code for the error type, surfaced via
Diagnostic::oopsie_exit_code and used by Report’s Termination impl when
the error is returned from main. Accepts an integer literal in 1..=255
(0 is rejected: it denotes success, contradictory on an error path).
On an enum it applies to every variant that does not set its own exit_code;
on a struct it is the struct’s exit code.
Forms: exit_code = N.
§Example
use oopsie::Diagnostic as _;
#[oopsie::oopsie]
#[oopsie(module(false), exit_code = 70)]
pub struct AppError {
detail: String,
}
let err = App { detail: String::from("boom") }.build();
assert_eq!(err.oopsie_exit_code().map(|n| n.get()), Some(70));§Variant / struct
| Keyword | Effect |
|---|---|
display | Display message |
transparent | Delegate to the wrapped error |
help | Static help text |
code | Error code |
exit_code | Process exit code |
provide | Provide a typed value |
vis | Selector visibility |
The short form #[oopsie("msg {field}")] is shorthand for display(...).
With the unstable feature, help and code are additionally surfaced
through the nightly Provider API.
Sets the variant’s (or struct’s) Display message using format! semantics,
with named ({field}) or positional ({}) interpolation over the item’s fields.
This is the long form of the short #[oopsie("msg")]; use it when combining
display with other keywords in the same attribute. Without any display, the
variant or struct name is used verbatim.
Forms: display = "...", display("..."), display("... {}", expr).
§Example
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie(display("{count} items missing from {place}"))]
Missing { count: u32, place: String },
}
let err = Missing { count: 3u32, place: "cache".to_owned() }.build();
assert_eq!(err.to_string(), "3 items missing from cache");Delegates Display and the error source to the wrapped error, and generates a
From<Inner> impl instead of a context selector — so the inner error converts
directly with ?. A transparent variant or struct must have exactly one field:
the source.
Forms: transparent, transparent = true, transparent = false.
§Example
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie(transparent)]
Io { source: std::io::Error },
}
fn read() -> Result<(), AppError> {
let _ = std::fs::read("/no/such/file")?;
Ok(())
}
let err = read().unwrap_err();
assert!(matches!(err, AppError::Io { .. }));Attaches static help text to the variant or struct, surfaced via
Diagnostic::oopsie_help_text and shown by Report. The text follows format!
semantics, so it can interpolate the item’s fields. For help derived from a
field’s runtime Display, use the field-level #[oopsie(help)] instead.
Forms: help = "...", help("..."), help("... {}", expr).
§Example
use oopsie::Diagnostic as _;
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie(display("config missing"), help = "run `app init` to create one")]
NoConfig,
}
let err = NoConfig.build();
assert_eq!(err.oopsie_help_text().as_deref(), Some("run `app init` to create one"));Attaches an error code to the variant or struct, surfaced via
Diagnostic::oopsie_error_code and shown by Report. The code follows
format! semantics, so it can interpolate the item’s fields. On a traced
type it replaces the auto-generated code for this item.
Forms: code = "...", code("..."), code("... {}", expr).
§Example
use oopsie::Diagnostic as _;
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie(display("bad request: {status}"), code("E{}", status))]
Http { status: u16 },
}
let err = Http { status: 404u16 }.build();
assert_eq!(err.oopsie_error_code().as_deref(), Some("E404"));Sets the process exit code for this variant, surfaced via
Diagnostic::oopsie_exit_code and used by Report’s Termination impl when
the error is returned from main. Accepts an integer literal in 1..=255
(0 is rejected: it denotes success, contradictory on an error path). A
container-level exit_code is the default for every variant; a variant’s own
value overrides it.
Forms: exit_code = N.
§Example
use oopsie::Diagnostic as _;
#[oopsie::oopsie]
#[oopsie(module(false), exit_code = 1)]
pub enum AppError {
#[oopsie("bad usage")]
Usage,
#[oopsie("config error: {path}")]
#[oopsie(exit_code = 78)]
Config { path: String },
}
assert_eq!(Usage.build().oopsie_exit_code().map(|n| n.get()), Some(1));
assert_eq!(
Config { path: String::from("x") }.build().oopsie_exit_code().map(|n| n.get()),
Some(78)
);Provides a value or reference for the variant or struct through the
std::error::Request provider API (active with the unstable feature), letting
callers retrieve typed data with Error::request_ref/request_value. The
expression is evaluated against the item’s fields; ref provides a borrow.
Forms: provide(Type => expr), provide(ref, Type => expr).
§Example
use oopsie::ErrorCode;
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie(display("query failed"))]
#[oopsie(provide(ErrorCode => ErrorCode::from_static("E_QUERY")))]
Query,
}
let err = Query.build();
assert_eq!(err.to_string(), "query failed");Overrides the visibility of this one variant’s selector, taking precedence over
any container-level vis(...). Useful for exposing a single selector from an
otherwise crate-private error.
Forms: vis(pub), vis(pub(crate)), vis = "pub(crate)".
§Example
#[oopsie::oopsie]
#[oopsie(module(errs))]
enum AppError {
#[oopsie("not found: {key}")]
#[oopsie(vis(pub(crate)))]
Missing { key: String },
}
fn main() {
let err = errs::Missing { key: "k".to_owned() }.build();
assert_eq!(err.to_string(), "not found: k");
}§Field
| Keyword | Effect |
|---|---|
from | Mark the chained source error |
capture | Auto-fill via Capturable |
provide | Provide a typed value |
backtrace | Captured backtrace field |
spantrace | Captured span-trace field |
traces | Packed (Backtrace, SpanTrace) field |
location | Captured caller location |
help | Dynamic help from this field’s Display |
A field named source is auto-detected as the chained source error, and a
Box<T> source is auto-unboxed so the selector accepts T (trait objects
exempt).
Marks this field as the chained source error, supplied to the selector at
construction (via .build_error(source) or .context(...)) rather than being a
selector field. A field named source is detected automatically; use from to
mark a differently-named field. from(false) opts a source-named field out.
from(Type, transform) accepts a Type and stores transform(value); a
Box<T> source is auto-unboxed so the selector takes T directly.
Forms: from, from(true), from(false), from(Type, transform).
§Example
use oopsie::Contextual as _;
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie("read failed")]
Read {
#[oopsie(from)]
cause: std::io::Error,
},
}
let err = Read.build_error(std::io::Error::other("disk"));
assert_eq!(err.to_string(), "read failed");Auto-fills this field via the Capturable trait when the error is constructed,
so it is excluded from the context selector instead of being caller-supplied.
Trace-typed fields (Backtrace, SpanTrace, packed traces) get this
automatically; capture(false) opts such a field back onto the selector.
Forms: capture, capture = true, capture = false, capture(true), capture(false).
§Example
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie("request {id} failed")]
Failed {
id: u32,
#[oopsie(capture)]
at: std::time::SystemTime,
},
}
// `at` is captured automatically, so the selector only takes `id`.
let err = Failed { id: 7u32 }.build();
assert_eq!(err.to_string(), "request 7 failed");Provides a value or reference derived from this field through the
std::error::Request provider API (active with the unstable feature), letting
callers retrieve typed data with Error::request_ref/request_value. The
expression names the field; ref provides a borrow of it.
Forms: provide(Type => expr), provide(ref, Type => expr).
§Example
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie("lookup failed: {key}")]
Lookup {
#[oopsie(provide(ref, str => key.as_str()))]
key: String,
},
}
let err = Lookup { key: "missing".to_owned() }.build();
assert_eq!(err.to_string(), "lookup failed: missing");Marks this field as the captured backtrace, surfaced via
Diagnostic::oopsie_backtrace. The field’s type must have Backtrace as its
last path segment; it is auto-captured (and excluded from the selector). Marking
a field is only needed when its type isn’t recognized automatically.
Forms: backtrace, backtrace = true, backtrace = false.
§Example
use oopsie::Backtrace;
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie("boom")]
Boom {
#[oopsie(backtrace)]
bt: Backtrace,
},
}
let err = Boom.build();
assert_eq!(err.to_string(), "boom");Marks this field as the captured span trace, surfaced via
Diagnostic::oopsie_spantrace. The field’s type must have SpanTrace as its
last path segment; it is auto-captured (and excluded from the selector). Marking
a field is only needed when its type isn’t recognized automatically.
Forms: spantrace, spantrace = true, spantrace = false.
§Example
use oopsie::SpanTrace;
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie("boom")]
Boom {
#[oopsie(spantrace)]
st: SpanTrace,
},
}
let err = Boom.build();
assert_eq!(err.to_string(), "boom");Marks this field as the packed (Backtrace, SpanTrace) pair, surfacing both
through Diagnostic::oopsie_backtrace and oopsie_spantrace from one field. The
type must be (Backtrace, SpanTrace) (optionally boxed); it is auto-captured and
excluded from the selector. A packed traces field cannot coexist with separate
backtrace/spantrace fields.
Forms: traces, traces = true, traces = false.
§Example
use oopsie::{Backtrace, SpanTrace};
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie("boom")]
Boom {
#[oopsie(traces)]
traces: (Backtrace, SpanTrace),
},
}
let err = Boom.build();
assert_eq!(err.to_string(), "boom");Marks this field as the captured caller location, surfaced via
Diagnostic::oopsie_location. The field’s type must be
&'static Location<'static> (the standard std::panic::Location); it is
auto-captured (and excluded from the selector). Marking a field is only needed
when its type isn’t recognized automatically.
Forms: location, location = true, location = false.
§Example
use std::panic::Location;
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie("boom")]
Boom {
#[oopsie(location)]
at: &'static Location<'static>,
},
}
let err = Boom.build();
assert_eq!(err.to_string(), "boom");Uses this field’s runtime Display output as the error’s dynamic help text,
surfaced via Diagnostic::oopsie_help_text. Unlike the variant-level
help = "...", the text comes from the field value at runtime rather than a
fixed string. At most one help field per variant or struct.
Forms: help, help = true, help = false.
§Example
use oopsie::Diagnostic as _;
#[oopsie::oopsie]
#[oopsie(module(false))]
pub enum AppError {
#[oopsie("config invalid")]
BadConfig {
#[oopsie(help)]
hint: String,
},
}
let err = BadConfig { hint: "set PORT in .env".to_owned() }.build();
assert_eq!(err.oopsie_help_text().as_deref(), Some("set PORT in .env"));§Generic error types
Type parameters, lifetimes, const parameters, bounds, and where clauses are
supported on both enums and structs. A context selector carries only the
parameters its captured fields reference: a field of type Vec<T> makes the
selector generic over T, while a leaf variant that mentions no parameter
stays non-generic and infers the destination’s parameters from the call site.
No bound is added beyond what you write. Interpolating a field in a display
(or help/code) format string requires that field’s type to implement the
formatting trait the placeholder uses, exactly as in any format! — if a
type parameter is interpolated as {field} but is not Display, the error
is the ordinary missing-Display bound at your format string. Add the bound
yourself (T: Display) when the message needs it.
#[derive(Debug, Oopsie)]
#[oopsie(module(false))]
enum Lookup<K: std::fmt::Debug> {
#[oopsie("no entry for {key:?}")]
Missing { key: K },
}
let err: Lookup<u32> = Missing { key: 7 }.build();
assert_eq!(err.to_string(), "no entry for 7");§Feature flags
| Feature | Default | Toolchain | Effect |
|---|---|---|---|
fancy | yes | stable | Report, colorized output, and the panic hook |
tracing | no | stable | span-trace capture and the tracing-subscriber error layer |
serde | no | stable | the erased module: serialize any error as a type-erased value |
chrono | no | stable | chrono::DateTime<Local> timestamps for traced(timestamp(chrono = true)) |
jiff | no | stable | jiff::Timestamp / jiff::Zoned timestamp capture |
extras | no | stable | the extras module: environment-snapshot Capturable helpers |
unstable | no | nightly | umbrella for the two unstable features below |
unstable-error-generic-member-access | no | nightly | trace and diagnostic surfacing through the Provider API |
unstable-try-trait-v2 | no | nightly | ? converts a failed Result directly into a Report |
Modules§
- backtrace
- Thread-local control over backtrace capture.
- erased
- Serializable, type-erased representations of errors.
- extras
- Ready-made
Capturabletypes for ambient process and environment state. - prelude
- Common imports for error handling at the call site.
- trace_
printer - Unified colored trace rendering for backtraces and span traces.
- tracing
tracing-subscriberintegration helpers.
Macros§
- start_
marker - Marks the start of meaningful backtraces on the current thread: frames
below this call are hidden from rendered error and panic traces. The
function containing the call stays visible. A later call replaces the
marker; other threads are unaffected.
RUST_BACKTRACE=fullrenders traces unfiltered, ignoring markers. Hiding is applied by thefancyfeature’s renderers; without that feature the marker is recorded but has no effect.
Structs§
- Backtrace
- A captured stack backtrace with deferred symbol resolution.
- Chain
- An iterator over an error and its chain of
sourcecauses. - Error
Code - An opaque error code, used for programmatic handling and matching.
- Help
Text - User-facing help text, intended to be shown in diagnostics.
- NoSource
- The stand-in “source” a leaf selector (one with no
sourcefield) builds from.Option::contextuses it as the source when there is no error value to attach. You normally reach a leaf error through.build()/.fail()/Option::context, so you rarely name this directly. - Optional
Span Trace - A wrapper around
Option<SpanTrace>that implementsCapturable. - Report
- A wrapper around an error that provides rich, colorized output.
- Span
Trace - A wrapper around
tracing_error::SpanTrace. - Style
- An opaque text style (foreground color plus modifiers) used by report themes.
- Theme
- A color theme: nine named colors plus the role accessors that paint a
report from them. Small and
Copy. - Welp
- A string-shaped error type for prototypes, one-off errors, and quick string contexts.
Enums§
- Color
Mode - Color output configuration.
- Rust
Backtrace - Whether backtrace capture is enabled, and how verbosely it should render.
Traits§
- AsError
Source - Normalize any source-error value to a
&(dyn Error + 'static). - Capturable
- A value that fills itself in when an error is constructed.
- Contextual
- Builds a target error from a context selector and a source error of type
E. - Diagnostic
- Trait for errors that expose diagnostic data.
- Error
Chain Ext - Walk an error and its
sourcechain. - Option
Ext - Extension trait on
Optionfor convertingNoneinto a typed error. - Result
Ext - Extension trait on
Resultfor ergonomic error context wrapping. - Welp
Option Ext - Extension trait on
Optionfor convertingNoneinto aWelp. - Welp
Result Ext - Extension trait on
Resultfor attaching a string message that produces aWelp.
Functions§
- get_
color_ mode - Get the current global color mode.
- get_
theme - The current process-global theme.
- install_
panic_ hook - Install a process-global panic hook that renders panics with a colored
message, span trace, and backtrace via
TracePrinter. - set_
color_ mode - Set the global color mode for all
Reportinstances usingAuto. - set_
theme - Replace the process-global theme used by
Reportand the panic hook. Affects every report that doesn’t carry a per-instance override.
Attribute Macros§
- oopsie
- Attribute macro — the primary way to define an
oopsieerror type.
Derive Macros§
- Oopsie
- Derive macro that generates context selectors,
Display, andErrorimpls for a struct or enum.