Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

751 Commits

Repository files navigation

masterror

Framework-agnostic application error types

Crates.iodocs.rsDownloadsMSRVLicenseREUSE statuscodecov

CIHits-of-CodeIMIR

🇷🇺 Читайте README на русском языке 🇰🇷 한국어 README


Table of Contents


Overview

masterror grew from a handful of helpers into a workspace of composable crates for building consistent, observable error surfaces across Rust services. The core crate stays framework-agnostic, while feature flags light up transport adapters, integrations and telemetry without pulling in heavyweight defaults. No unsafe, MSRV is pinned, and the derive macros keep your domain types in charge of redaction and metadata.

Go to top

Highlights

  • Unified taxonomy.AppError, AppErrorKind and AppCode model domain and transport concerns with conservative HTTP/gRPC mappings, turnkey retry/auth hints and RFC7807 output via ProblemJson.
  • Native derives.#[derive(Error)] and #[derive(Masterror)] wire custom types into the runtime types. #[derive(Masterror)] with #[masterror(...)] forwards sources, backtraces, telemetry fields and redaction policy; #[app_error] maps a derived error to an AppErrorKind/AppCode (optionally exposing its Display message) and #[provide] registers typed telemetry providers on the domain error itself.
  • Typed telemetry.Metadata stores structured key/value context (strings, integers, floats, durations, IP addresses and optional JSON) with per-field redaction controls and builders in field::*, so logs stay structured without manual String maps.
  • Transport adapters. Optional features expose Actix/Axum responders, tonic::Status conversions, WASM/browser logging and OpenAPI schema generation without contaminating the lean default build.
  • Battle-tested integrations. Enable focused mappings for sqlx, reqwest, redis, validator, config, tokio, teloxide, multipart, Telegram init-data validation and more — each translating library errors into the taxonomy with telemetry attached.
  • Turnkey defaults. The turnkey module ships a ready-to-use error catalog, a heuristic classifier and conservative mappings into the canonical taxonomy for teams that want a consistent baseline out of the box.
  • Boxed error interop.AppError::from_boxed adopts any Box<dyn Error + Send + Sync> without re-boxing and into_boxed_dyn_error converts back, matching anyhow's round-trip API while keeping the source chain intact and downcastable.
  • Typed control-flow macros.ensure! and fail! short-circuit functions with your domain errors without allocating or formatting on the happy path, while app_error! builds ad-hoc AppError values with format!-style messages in expression position.
Go to top

Workspace Crates

CrateWhat it providesWhen to depend on it
masterrorCore error types, metadata builders, transports, integrations and the prelude.Application crates, services and libraries that want a stable error surface.
masterror-deriveProc-macros backing #[derive(Error)], #[derive(Masterror)], #[app_error] and #[provide].Brought in automatically via masterror; depend directly only for macro hacking.
masterror-templateShared template parser used by the derive macros for formatter analysis.Internal dependency; reuse when you need the template parser elsewhere.
Go to top

Feature Flags

Pick only what you need; the default feature set is just std, everything else is opt-in.

  • Web transports:axum, actix, multipart, openapi, serde_json.
  • Telemetry & observability:tracing, metrics, backtrace, colored for colored terminal output.
  • Async & IO integrations:tokio, reqwest, sqlx, sqlx-migrate, redis, validator, config.
  • Messaging & bots:teloxide, init-data for Telegram Mini App init-data validation via init-data-rs.
  • Front-end tooling:frontend for WASM/browser console logging.
  • gRPC:tonic to emit tonic::Status responses.
  • Batteries included:turnkey to adopt the pre-built taxonomy and helpers.

The build script keeps the full feature snippet below in sync with Cargo.toml.

Go to top

Installation

[dependencies]
masterror = { version = "0.29.0", default-features = false }
# or with features:# masterror = { version = "0.29.0", features = [# "std", "axum", "actix", "openapi",# "serde_json", "tracing", "metrics", "backtrace",# "colored", "sqlx", "sqlx-migrate", "reqwest",# "redis", "validator", "config", "tokio",# "multipart", "teloxide", "init-data", "tonic",# "frontend", "turnkey", "benchmarks"# ] }
Go to top

Benchmarks

Criterion benchmarks cover the hottest conversion paths so regressions are visible before shipping. Run them locally with:

cargo bench -F benchmarks --bench error_paths

The suite emits two groups:

  • context_into_error/* promotes a dummy source error with representative metadata (strings, counters, durations, IPs) through ResultExt::ctx in both redacted and non-redacted modes.
  • problem_json_from_app_error/* consumes the resulting AppError values to build RFC 7807 payloads via ProblemJson::from_app_error, showing how message redaction and field policies impact serialization.

Adjust Criterion CLI flags (for example --sample-size 200 or --save-baseline local) after -- to trade throughput for tighter confidence intervals when investigating changes.

Go to top

Code Coverage

codecov

Coverage reports are automatically generated on every CI run and uploaded to Codecov. The project maintains high test coverage across all modules to ensure reliability and catch regressions early.

Coverage Visualizations

Sunburst Graph

The inner-most circle represents the entire project, moving outward through folders to individual files. Size and color indicate statement count and coverage percentage.

Sunburst

Grid View

Each block represents a single file. Block size and color correspond to statement count and coverage percentage.

Grid

Icicle Chart

Hierarchical view starting with the entire project at the top, drilling down through folders to individual files. Size and color reflect statement count and coverage.

Icicle

Go to top

Quick Start

Create an error

Create an error:

use masterror::{AppError,AppErrorKind, field};let err = AppError::new(AppErrorKind::BadRequest,"Flag must be set");assert!(matches!(err.kind,AppErrorKind::BadRequest));let err_with_meta = AppError::service("downstream").with_field(field::str("request_id","abc123"));assert_eq!(err_with_meta.metadata().len(),1);let err_with_context = AppError::internal("db down").with_context(std::io::Error::new(std::io::ErrorKind::Other,"boom"));assert!(err_with_context.source_ref().is_some());

With prelude:

use masterror::prelude::*;fndo_work(flag:bool) -> AppResult<()>{if !flag {returnErr(AppError::bad_request("Flag must be set"));}Ok(())}
Go to top

Advanced Usage

Fail fast without sacrificing typing

ensure! and fail! provide typed alternatives to the formatting-heavy anyhow::ensure!/anyhow::bail! helpers. They evaluate the error expression only when the guard trips, so success paths stay allocation-free.

use masterror::{AppError,AppErrorKind,AppResult};fnguard(flag:bool) -> AppResult<()>{
masterror::ensure!(flag,AppError::bad_request("flag must be set"));Ok(())}fnbail() -> AppResult<()>{
masterror::fail!(AppError::unauthorized("token expired"));}assert!(guard(true).is_ok());assert!(matches!(guard(false).unwrap_err().kind,AppErrorKind::BadRequest));assert!(matches!(bail().unwrap_err().kind,AppErrorKind::Unauthorized));

app_error! is the anyhow::anyhow! counterpart: an expression macro that builds an AppError from a kind and an optional format!-style message with implicit capture. The kind-only form performs no allocation; the message form allocates exactly once.

use masterror::{AppErrorKind,AppResult, app_error};fnfind(id:u64) -> AppResult<u64>{None::<u64>.ok_or_else(|| app_error!(AppErrorKind::NotFound,"no entity {id}"))}let bare = app_error!(AppErrorKind::Timeout);assert!(bare.message.is_none());assert!(matches!(find(7).unwrap_err().kind,AppErrorKind::NotFound));
Derive domain errors and map them to transports

masterror ships native derives so your domain types stay expressive while the crate handles conversions, telemetry and redaction for you.

use std::io;use masterror::Error;#[derive(Debug,Error)]#[error("I/O failed: {source}")]pubstructDomainError{#[from]#[source]source: io::Error,}#[derive(Debug,Error)]#[error(transparent)]pubstructWrappedDomainError(#[from]#[source]DomainError);fnload() -> Result<(),DomainError>{Err(io::Error::other("disk offline").into())}let err = load().unwrap_err();assert_eq!(err.to_string(),"I/O failed: disk offline");let wrapped = WrappedDomainError::from(err);assert_eq!(wrapped.to_string(),"I/O failed: disk offline");
  • use masterror::Error; brings the derive macro into scope.
  • #[from] automatically implements From<...> while ensuring wrapper shapes are valid.
  • #[error(transparent)] enforces single-field wrappers that forward Display/source to the inner error.
  • #[app_error(kind = AppErrorKind::..., code = AppCode::..., message)] maps the derived error into AppError/AppCode. The optional code = ... arm emits an AppCode conversion, while the message flag forwards the derived Display 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 on thiserror.
  • TemplateFormatter mirrors thiserror'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() and TemplateFormatter::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.
Attach telemetry, redaction policy and conversions

#[derive(Masterror)] wires a domain error into [masterror::Error], adds metadata, redaction policy and optional transport mappings. The accompanying #[masterror(...)] attribute mirrors the #[app_error] syntax while staying explicit about telemetry and redaction.

use masterror::{
mapping::HttpMapping,AppCode,AppErrorKind,Error,Masterror,MessageEditPolicy};#[derive(Debug,Masterror)]#[error("user {user_id} missing flag {flag}")]#[masterror( code = AppCode::NotFound, category = AppErrorKind::NotFound, message, redact(message, fields("user_id" = hash)), telemetry(Some(masterror::field::str("user_id", user_id.clone())), attempt.map(|value| masterror::field::u64("attempt", value))), map.grpc = 5, map.problem = "https://errors.example.com/not-found")]structMissingFlag{user_id:String,flag:&'staticstr,attempt:Option<u64>,#[source]source:Option<std::io::Error>}let err = MissingFlag{user_id:"alice".into(),flag:"beta",attempt:Some(2),source:None};let converted:Error = err.into();assert_eq!(converted.code,AppCode::NotFound);assert_eq!(converted.kind,AppErrorKind::NotFound);assert_eq!(converted.edit_policy,MessageEditPolicy::Redact);assert!(converted.metadata().get("user_id").is_some());assert_eq!(MissingFlag::HTTP_MAPPING,HttpMapping::new(AppCode::NotFound,AppErrorKind::NotFound));
  • code / category pick the public [AppCode] and internal [AppErrorKind].
  • message forwards the formatted [Display] output as the safe public message. Omit it to keep the message private.
  • redact(message) flips [MessageEditPolicy] to redactable at the transport boundary, fields("name" = hash, "card" = last4) overrides metadata policies (hash, last4, redact, none).
  • telemetry(...) accepts expressions that evaluate to Option<masterror::Field>. Each populated field is inserted into the resulting [Metadata]; use telemetry() when no fields are attached.
  • map.grpc / map.problem capture optional gRPC status codes (as i32) and RFC 7807 type URIs. The derive emits tables such as MyError::HTTP_MAPPING, MyError::GRPC_MAPPING and MyError::PROBLEM_MAPPING (or slice variants for enums) for downstream integrations.

All familiar field-level attributes (#[from], #[source], #[backtrace]) are still honoured. Sources and backtraces are automatically attached to the generated [masterror::Error].

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. The generated From conversions produce an AppError carrying the mapped kind and code (plus the Display output as public message when the message flag is set) and attach the original domain error as the source: it stays downcastable via downcast_ref, shows up in chain()/root_cause(), and its #[provide] data is forwarded through the AppError on toolchains with error_generic_member_access. Source attachment requires the domain error to be Send + Sync + 'static; add the no_source flag to #[app_error(...)] to opt out and drop the domain error during conversion instead.

request_ref/request_value and the std::error::Request machinery require a nightly toolchain (error_generic_member_access); the crate detects compiler support at build time and only enables provider integration when available.

use std::error::request_ref;use masterror::{AppCode,AppError,AppErrorKind,Error};#[derive(Clone,Debug,PartialEq,Eq)]structTelemetrySnapshot{name:&'staticstr,value:u64,}#[derive(Debug,Error)]#[error("structured telemetry {snapshot:?}")]#[app_error(kind = AppErrorKind::Service, code = AppCode::Service)]structStructuredTelemetryError{#[provide(ref = TelemetrySnapshot, value = TelemetrySnapshot)]snapshot:TelemetrySnapshot,}let err = StructuredTelemetryError{snapshot:TelemetrySnapshot{name:"db.query",value:42,},};let snapshot = request_ref::<TelemetrySnapshot>(&err).expect("telemetry");assert_eq!(snapshot.value,42);let app:AppError = err.into();assert!(matches!(app.kind,AppErrorKind::Service));

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 masterror::{AppCode,AppErrorKind,Error};#[derive(Debug,Error)]#[error("optional telemetry {telemetry:?}")]#[app_error(kind = AppErrorKind::Internal, code = AppCode::Internal)]structOptionalTelemetryError{#[provide(ref = TelemetrySnapshot, value = TelemetrySnapshot)]telemetry:Option<TelemetrySnapshot>,}let noisy = OptionalTelemetryError{telemetry:Some(TelemetrySnapshot{name:"queue.depth",value:17,}),};let silent = OptionalTelemetryError{telemetry:None};assert!(request_ref::<TelemetrySnapshot>(&noisy).is_some());assert!(request_ref::<TelemetrySnapshot>(&silent).is_none());

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:

#[derive(Debug,Error)]enumEnumTelemetryError{#[error("named {label}")]#[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound)]Named{label:&'staticstr,#[provide(ref = TelemetrySnapshot)]snapshot:TelemetrySnapshot,},#[error("optional tuple")]#[app_error(kind = AppErrorKind::Timeout, code = AppCode::Timeout)]Optional(#[provide(ref = TelemetrySnapshot)]Option<TelemetrySnapshot>),#[error("owned tuple")]#[app_error(kind = AppErrorKind::Service, code = AppCode::Service)]Owned(#[provide(value = TelemetrySnapshot)]TelemetrySnapshot),}let owned = EnumTelemetryError::Owned(TelemetrySnapshot{name:"redis.latency",value:3,});let app:AppError = owned.into();assert!(matches!(app.kind,AppErrorKind::Service));

Compared to thiserror, you retain the familiar deriving surface while gaining structured telemetry (#[provide]) and first-class conversions into AppError/AppCode without manual glue.

Problem JSON payloads and retry/authentication hints
use masterror::{AppError,AppErrorKind,ProblemJson};let problem = ProblemJson::from_app_error(AppError::new(AppErrorKind::Unauthorized,"Token expired").with_retry_after_secs(30).with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#));assert_eq!(problem.status,401);assert_eq!(problem.retry_after,Some(30));assert_eq!(problem.grpc.expect("grpc").name,"UNAUTHENTICATED");
Environment detection with DisplayMode

DisplayMode detects the deployment environment (Prod, Local or Staging) and drives the Display output of AppError. DisplayMode::current() resolves the mode in this order and caches the result on first access (the environment is read once per process):

  1. MASTERROR_ENV environment variable (prod/production, local/dev/development, or staging/stage)
  2. KUBERNETES_SERVICE_HOST presence (selects Prod)
  3. Build configuration (debug_assertionsLocal, release → Prod; the only detection step available without the std feature)
use masterror::DisplayMode;let mode = DisplayMode::current();match mode {DisplayMode::Prod => println!("Running in production mode"),DisplayMode::Local => println!("Running in local development mode"),DisplayMode::Staging => println!("Running in staging mode"),}

Display for AppError dispatches on the detected mode:

ModeLayout
LocalMulti-line human-readable report: kind, code, message, source chain, metadata
ProdCompact single-line JSON: kind, code, optional message, metadata
StagingSame JSON as Prod plus a source_chain array

All layouts apply per-field redaction policies: Redact renders the [REDACTED] placeholder, Hash renders a SHA-256 hex digest, Last4 keeps only the trailing characters (fields that cannot be masked are omitted). Set MASTERROR_ENV=local on any host to force the human-readable layout, e.g. when debugging inside Kubernetes.

In Local mode the output looks like:

Error: Not found
Code: NOT_FOUND
Message: User not found
Context:
user_id: 12345
request_id: abc-def

In Prod mode the same error renders as:

{"kind":"NotFound","code":"NOT_FOUND","message":"User not found","metadata":{"request_id":"abc-def","user_id":12345}}

Colored Terminal Output:

Enable the colored feature for ANSI styling with automatic TTY detection. It affects only the Local layout; Prod and Staging JSON never contains escape sequences:

[dependencies]
masterror = { version = "0.29.0", features = ["colored"] }
Go to top

Examples

Comprehensive real-world examples demonstrating masterror integration with popular frameworks:

ExampleDescriptionFeatures
axum-rest-apiREST API with RFC 7807 Problem DetailsHTTP endpoints, domain errors, integration tests
sqlx-databaseDatabase error handling with SQLxConnection errors, constraint violations, transactions
custom-domain-errorsPayment processing domain errorsDerive macro, error conversion, structured errors
basic-asyncAsync error handling with tokioError propagation, timeout handling, Result types

All examples are runnable; the axum-rest-api example additionally ships integration tests. See the examples/ directory for complete source code and documentation.

Go to top

Resources

  • Explore the error-handling wiki for step-by-step guides, comparisons with thiserror/anyhow, and troubleshooting recipes.
  • Browse the crate documentation on docs.rs for API details, feature-specific guides and transport tables.
  • Check CHANGELOG.md for release highlights and migration notes.
  • Review RustManifest for the development standards and best practices this project follows.
Go to top

Metrics

Metrics

Go to top

License

MSRV: 1.96 · License: MIT · No unsafe

About

Framework-agnostic error types for Rust services.

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages