masterror Β· Framework-agnostic application error types
π·πΊ Π§ΠΈΡΠ°ΠΉΡΠ΅ README Π½Π° ΡΡΡΡΠΊΠΎΠΌ ΡΠ·ΡΠΊΠ΅.
Small, pragmatic error model for API-heavy Rust services.
Core is framework-agnostic; integrations are opt-in via feature flags.
Stable categories, conservative HTTP mapping, no unsafe
.
- Core types:
AppError
,AppErrorKind
,AppResult
,AppCode
,ErrorResponse
- Optional Axum/Actix integration
- Optional OpenAPI schema (via
utoipa
) - Conversions from
sqlx
,reqwest
,redis
,validator
,config
,tokio
TL;DR
[]
= { = "0.10.8", = false }
# or with features:
# masterror = { version = "0.10.8", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
# "teloxide", "telegram-webapp-sdk", "frontend", "turnkey"
# ] }
Since v0.5.0: derive custom errors via #[derive(Error)]
(use masterror::Error;
) and inspect browser logging failures with BrowserConsoleError::context()
.
Since v0.4.0: optional frontend
feature for WASM/browser console logging.
Since v0.3.0: stable AppCode
enum and extended ErrorResponse
with retry/authentication metadata.
- Stable taxonomy. Small set of
AppErrorKind
categories mapping conservatively to HTTP. - Framework-agnostic. No assumptions, no
unsafe
, MSRV pinned. - Opt-in integrations. Zero default features; you enable what you need.
- Clean wire contract.
ErrorResponse { status, code, message, details?, retry?, www_authenticate? }
. - One log at boundary. Log once with
tracing
. - Less boilerplate. Built-in conversions, compact prelude, and the
native
masterror::Error
derive with#[from]
/#[error(transparent)]
support. - Consistent workspace. Same error surface across crates.
[]
# lean core
= { = "0.10.8", = false }
# with Axum/Actix + JSON + integrations
# masterror = { version = "0.10.8", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
# "teloxide", "telegram-webapp-sdk", "frontend", "turnkey"
# ] }
MSRV: 1.90 No unsafe: forbidden by crate.
Create an error:
use ;
let err = new;
assert!;
With prelude:
use *;
use io;
use Error;
;
let err = load.unwrap_err;
assert_eq!;
let wrapped = from;
assert_eq!;
use masterror::Error;
brings the crate's derive macro into scope.#[from]
automatically implementsFrom<...>
while ensuring wrapper shapes are valid.#[error(transparent)]
enforces single-field wrappers that forwardDisplay
/source
to the inner error.#[app_error(kind = AppErrorKind::..., code = AppCode::..., message)]
maps the derived error intoAppError
/AppCode
. The optionalcode = ...
arm emits anAppCode
conversion, while themessage
flag forwards the derivedDisplay
output as the public message instead of producing a bare error.masterror::error::template::ErrorTemplate
parses#[error("...")]
strings, exposing literal and placeholder segments so custom derives can be implemented without relying onthiserror
.TemplateFormatter
mirrorsthiserror
's formatter detection so existing derives that relied on hexadecimal, pointer or exponential renderers keep compiling.- Display placeholders preserve their raw format specs via
TemplateFormatter::display_spec()
andTemplateFormatter::format_fragment()
, so derived code can forward:>8
,:.3
and other display-only options without reconstructing the original string. TemplateFormatterKind
exposes the formatter trait requested by a placeholder, making it easy to branch on the requested rendering behaviour without manually matching every enum variant.
Display shorthand projections
#[error("...")]
supports the same shorthand syntax as thiserror
for
referencing fields with .field
or .0
. The derive now understands chained
segments, so projections like .limits.lo
, .0.data
or
.suggestion.as_ref().map_or_else(...)
keep compiling unchanged. Raw
identifiers and tuple indices are preserved, ensuring keywords such as
r#type
and tuple fields continue to work even when you call methods on the
projected value.
use Error;
AppError conversions
Annotating structs or enum variants with #[app_error(...)]
captures the
metadata required to convert the domain error into AppError
(and optionally
AppCode
). Every variant in an enum must provide the mapping when any variant
requests it.
use ;
let app: AppError = MissingFlag .into;
assert!;
assert_eq!;
let code: AppCode = MissingFlag .into;
assert!;
For enums, each variant specifies the mapping while the derive generates a
single From<Enum>
implementation that matches every variant:
let missing = Missing ;
let as_app: AppError = missing.into;
assert_eq!;
Structured telemetry providers and AppError mappings
#[provide(...)]
exposes typed context through std::error::Request
, while
#[app_error(...)]
records how your domain error translates into AppError
and AppCode
. The derive mirrors thiserror
's syntax and extends it with
optional telemetry propagation and direct conversions into the masterror
runtime types.
use request_ref;
use ;
let err = StructuredTelemetryError ;
let snapshot = .expect;
assert_eq!;
let app: AppError = err.into;
let via_app = .expect;
assert_eq!;
Optional telemetry only surfaces when present, so None
does not register a
provider. Owned snapshots can still be provided as values when the caller
requests ownership:
use ;
let noisy = OptionalTelemetryError ;
let silent = OptionalTelemetryError ;
assert!;
assert!;
Enums support per-variant telemetry and conversion metadata. Each variant chooses
its own AppErrorKind
/AppCode
mapping while the derive generates a single
From<Enum>
implementation:
let owned = Owned;
let app: AppError = owned.into;
assert!;
Compared to thiserror
, you retain the familiar deriving surface while gaining
structured telemetry (#[provide]
) and first-class conversions into
AppError
/AppCode
without writing manual From
implementations.
Formatter traits
Placeholders default to Display
({value}
) but can opt into richer
formatters via the same specifiers supported by thiserror
v2.
TemplateFormatter::is_alternate()
tracks the #
flag, while
TemplateFormatterKind
exposes the underlying core::fmt
trait so derived
code can branch on the requested renderer without manual pattern matching.
Unsupported formatters surface a compile error that mirrors thiserror
's
diagnostics.
Specifier | core::fmt trait |
Example output | Notes |
---|---|---|---|
default | core::fmt::Display |
value |
User-facing strings; # has no effect. |
:? / :#? |
core::fmt::Debug |
Struct { .. } / multi-line |
Mirrors Debug ; # pretty-prints structs. |
:x / :#x |
core::fmt::LowerHex |
0x2a |
Hexadecimal; # prepends 0x . |
:X / :#X |
core::fmt::UpperHex |
0x2A |
Uppercase hex; # prepends 0x . |
:p / :#p |
core::fmt::Pointer |
0x1f00 / 0x1f00 |
Raw pointers; # is accepted for compatibility. |
:b / :#b |
core::fmt::Binary |
101010 / 0b101010 |
Binary; # prepends 0b . |
:o / :#o |
core::fmt::Octal |
52 / 0o52 |
Octal; # prepends 0o . |
:e / :#e |
core::fmt::LowerExp |
1.5e-2 |
Scientific notation; # forces the decimal point. |
:E / :#E |
core::fmt::UpperExp |
1.5E-2 |
Uppercase scientific; # forces the decimal point. |
TemplateFormatterKind::supports_alternate()
reports whether the#
flag is meaningful for the requested trait (pointer accepts it even though the output matches the non-alternate form).TemplateFormatterKind::specifier()
returns the canonical format specifier character when one exists, enabling custom derives to re-render placeholders in their original style.TemplateFormatter::from_kind(kind, alternate)
reconstructs a formatter from the lightweightTemplateFormatterKind
, making it easy to toggle the alternate flag in generated code.
use ptr;
use Error;
let err = FormattedError ;
let rendered = err.to_string;
assert!;
assert!;
assert!;
assert!;
assert!;
assert!;
assert!;
use ;
let template = parse.expect;
let mut placeholders = template.placeholders;
let code = placeholders.next.expect;
let code_formatter = code.formatter;
assert!;
let code_kind = code_formatter.kind;
assert_eq!;
assert!;
assert_eq!;
assert!;
let lowered = from_kind;
assert!;
let payload = placeholders.next.expect;
let payload_formatter = payload.formatter;
assert_eq!;
let payload_kind = payload_formatter.kind;
assert_eq!;
assert_eq!;
assert!;
let pretty_debug = from_kind;
assert!;
assert!;
Display-only format specs (alignment, precision, fill β including #
as a fill
character) are preserved so you can forward them to write!
without rebuilding
the fragment:
use ErrorTemplate;
let aligned = parse.expect;
let display = aligned.placeholders.next.expect;
assert_eq!;
assert_eq!;
let hashed = parse.expect;
let hash_placeholder = hashed
.placeholders
.next
.expect;
assert_eq!;
assert_eq!;
Compatibility with
thiserror
v2: the derive understands the extended formatter set introduced inthiserror
2.x and reports identical diagnostics for unsupported specifiers, so migrating existing derives is drop-in.
use ;
let template = parse.expect;
let display = template.display_with;
assert_eq!;
use ;
use Duration;
let app_err = new;
let resp: ErrorResponse = .into
.with_retry_after_duration
.with_www_authenticate;
assert_eq!;
// features = ["axum", "serde_json"]
...
assert!;
Ok
}
- On non-WASM targets
log_to_browser_console
returnsBrowserConsoleError::UnsupportedTarget
. BrowserConsoleError::context()
exposes optional browser diagnostics for logging/telemetry when console logging fails.
axum
β IntoResponse integration with structured JSON bodiesactix
β Actix Web ResponseError and Responder implementationsopenapi
β Generate utoipa OpenAPI schema for ErrorResponseserde_json
β Attach structured JSON details to AppErrorsqlx
β Classify sqlx_core::Error variants into AppError kindssqlx-migrate
β Map sqlx::migrate::MigrateError into AppError (Database)reqwest
β Classify reqwest::Error as timeout/network/external APIredis
β Map redis::RedisError into cache-aware AppErrorvalidator
β Convert validator::ValidationErrors into validation failuresconfig
β Propagate config::ConfigError as configuration issuestokio
β Classify tokio::time::error::Elapsed as timeoutmultipart
β Handle axum multipart extraction errorsteloxide
β Convert teloxide_core::RequestError into domain errorstelegram-webapp-sdk
β Surface Telegram WebApp validation failuresfrontend
β Log to the browser console and convert to JsValue on WASMturnkey
β Ship Turnkey-specific error taxonomy and conversions
std::io::Error
β InternalString
β BadRequestsqlx::Error
β NotFound/Databaseredis::RedisError
β Cachereqwest::Error
β Timeout/Network/ExternalApiaxum::extract::multipart::MultipartError
β BadRequestvalidator::ValidationErrors
β Validationconfig::ConfigError
β Configtokio::time::error::Elapsed
β Timeoutteloxide_core::RequestError
β RateLimited/Network/ExternalApi/Deserialization/Internaltelegram_webapp_sdk::utils::validate_init_data::ValidationError
β TelegramAuth
Minimal core:
= { = "0.10.8", = false }
API (Axum + JSON + deps):
= { = "0.10.8", = [
"axum", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
API (Actix + JSON + deps):
= { = "0.10.8", = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
// features = ["turnkey"]
use ;
use ;
// Classify a raw SDK/provider error
let kind = classify_turnkey_error;
assert!;
// Wrap into AppError
let e = new;
let app: AppError = e.into;
assert_eq!;
- Use
ErrorResponse::new(status, AppCode::..., "msg")
instead of legacy - New helpers:
.with_retry_after_secs
,.with_retry_after_duration
,.with_www_authenticate
ErrorResponse::new_legacy
is temporary shim
Semantic versioning. Breaking API/wire contract β major bump. MSRV = 1.90 (may raise in minor, never in patch).
cargo +nightly fmt --
cargo clippy -- -D warnings
cargo test --all
cargo build
(regenerates README.md from the template)cargo doc --no-deps
cargo package --locked
- Not a general-purpose error aggregator like
anyhow
- Not a replacement for your domain errors
Apache-2.0 OR MIT, at your option.