Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

masterror

Framework-agnostic application error types with stable codes, HTTP/gRPC mappings and built-in telemetry

English Русский ν•œκ΅­μ–΄

Crates.io docs.rs MSRV License


What is masterror?

masterror is an error-handling workspace for Rust services that need more than Display and source(). Where thiserror stops at deriving trait implementations and anyhow stops at type-erased propagation, masterror carries an error all the way to the transport boundary:

  • AppError β€” a rich error value with a semantic category (AppErrorKind), a stable machine-readable code (AppCode), an optional safe public message, structured metadata and transport hints (Retry-After, WWW-Authenticate).
  • Conservative HTTP and gRPC mappings β€” every kind and code maps deterministically to an HTTP status, a tonic::Code discriminant and an RFC 7807 type URI.
  • Typed telemetry β€” metadata is stored as typed fields (strings, integers, floats, durations, IPs, UUIDs, JSON) with per-field redaction policies, not ad-hoc String maps.
  • Native derives β€” #[derive(Error)] mirrors thiserror syntax, while #[app_error(...)] and #[derive(Masterror)] wire domain errors into AppError with codes, categories, redaction and mapping tables.
  • Redaction by design β€” sources are never serialized to clients; messages, details and metadata fields can be redacted, hashed or masked at the boundary.

No unsafe, pinned MSRV, no_std support with the default std feature disabled.

The problem it solves

Concernthiserroranyhowmasterror
Display / source() derivesYesβ€”Yes (same syntax)
Type-erased propagation with contextβ€”YesYes (.ctx() / .context())
Stable machine-readable error codesManualManualAppCode, part of the wire contract
HTTP status mappingManualManualAppErrorKind::http_status(), stable table
gRPC status mappingManualManualCODE_MAPPINGS, tonic::Status conversion
RFC 7807 problem+jsonManualManualProblemJson::from_app_error
Structured, typed metadataβ€”β€”Metadata + field::* builders
Redaction of secrets at the boundaryβ€”β€”MessageEditPolicy, FieldRedaction
tracing / metrics / backtrace emissionβ€”β€”Feature-gated, automatic on construction

A thiserror-derived enum tells you what happened. masterror also decides what the client sees (status, code, safe message, problem+json), what operators see (structured fields, tracing events, counters) and what never leaks (sources, redacted fields).

Feature highlights

AreaWhat you get
Core taxonomyAppError, AppErrorKind (23 stable categories), AppCode (SCREAMING_SNAKE_CASE codes, custom codes supported), AppResult<T>
Derives#[derive(Error)], #[derive(Masterror)], #[app_error(...)], #[masterror(...)], #[provide(...)] telemetry providers
Control flowensure! / fail! β€” typed, allocation-free early returns
ContextResultExt::ctx / ResultExt::context, Context builder with caller tracking
Wire payloadsErrorResponse (legacy JSON), ProblemJson (RFC 7807) with retry and auth hints
TransportsAxum IntoResponse, Actix ResponseError/Responder, tonic::Status, WASM JsValue, OpenAPI schema
Integrationssqlx, redis, reqwest, validator, config, tokio, teloxide, Telegram Mini Apps init data, Turnkey
Observabilitytracing events, metrics counters, lazy backtrace capture, colored terminal output, DisplayMode (prod/staging/local)

Quick example

use masterror::{AppError, AppErrorKind, AppResult, ProblemJson, field};

fn find_user(id: u64) -> AppResult<()> {
    masterror::ensure!(id != 0, AppError::bad_request("id must be non-zero"));

    Err(AppError::not_found("user not found")
        .with_field(field::u64("user_id", id))
        .with_field(field::str("request_id", "abc123")))
}

let err = find_user(42).unwrap_err();
assert_eq!(err.kind, AppErrorKind::NotFound);
assert_eq!(err.kind.http_status(), 404);

let problem = ProblemJson::from_app_error(err);
assert_eq!(problem.status, 404);
assert_eq!(problem.code.as_str(), "NOT_FOUND");
assert_eq!(problem.grpc.expect("grpc").name, "NOT_FOUND");

Or declare a domain error once and let the derive handle the mapping:

use masterror::{AppCode, AppError, AppErrorKind, Error};

#[derive(Debug, Error)]
#[error("missing flag: {name}")]
#[app_error(kind = AppErrorKind::BadRequest, code = AppCode::BadRequest, message)]
struct MissingFlag {
    name: &'static str
}

let app: AppError = MissingFlag { name: "feature" }.into();
assert!(matches!(app.kind, AppErrorKind::BadRequest));

Workspace crates

CrateRole
masterrorCore error types, metadata, transports, integrations, prelude
masterror-deriveProc-macros behind #[derive(Error)] and #[derive(Masterror)] (pulled in automatically)
masterror-templateShared #[error("...")] template parser

Documentation

Getting started

Core concepts

Integrations

Advanced

  • No-Std β€” running without the standard library
  • Best Practices β€” patterns for services and libraries
  • Migration β€” moving from thiserror / anyhow