Crate masterror

Crate masterror 

Source
Expand description

Framework-agnostic application error types for backend services.

§Overview

A small, pragmatic error model designed for API-heavy services.
The core is framework-agnostic; integrations are optional and enabled via feature flags.

Core types:

  • AppError — rich error capturing code, taxonomy, message, metadata and transport hints
  • AppErrorKind — stable internal taxonomy of application errors
  • AppResult — convenience alias for returning AppError
  • ProblemJson — RFC7807 payload emitted by HTTP/gRPC adapters
  • ErrorResponse — legacy wire-level JSON payload for HTTP APIs
  • AppCode — public, machine-readable error code for clients
  • Metadata — structured telemetry attached to AppError
  • field — helper functions to build Metadata without manual enums

Key properties:

  • Stable, predictable error categories (AppErrorKind).
  • Explicit, overridable machine-readable codes (AppCode).
  • Structured metadata for observability without ad-hoc String maps.
  • Conservative and stable HTTP mappings.
  • Internal error sources are never serialized to clients (only logged).
  • Messages are safe to expose (human-oriented, non-sensitive).

§Minimum Supported Rust Version (MSRV)

MSRV is 1.90. New minor releases may increase MSRV with a changelog note, but never in a patch release.

§Feature flags

Enable only what you need:

  • axum — implements IntoResponse for AppError and ProblemJson with RFC7807 body
  • actix — implements Responder for ProblemJson and Actix ResponseError for AppError
  • tonic — converts Error into tonic::Status with sanitized metadata
  • openapi — derives an OpenAPI schema for ErrorResponse (via utoipa)
  • sqlxFrom<sqlx::Error> mapping
  • redisFrom<redis::RedisError> mapping
  • validatorFrom<validator::ValidationErrors> mapping
  • configFrom<config::ConfigError> mapping
  • tokioFrom<tokio::time::error::Elapsed> mapping
  • reqwestFrom<reqwest::Error> mapping
  • teloxideFrom<teloxide_core::RequestError> mapping
  • telegram-webapp-sdkFrom<telegram_webapp_sdk::utils::validate_init_data::ValidationError> mapping
  • frontend — convert errors into wasm_bindgen::JsValue and emit console.error logs in WASM/browser contexts
  • serde_json — support for structured JSON details in ErrorResponse and ProblemJson; also pulled transitively by axum
  • multipart — compatibility flag for Axum multipart
  • turnkey — domain taxonomy and conversions for Turnkey errors, exposed in the turnkey module

§Derive macros and telemetry

The masterror::Error derive mirrors thiserror while adding #[app_error] and #[provide] attributes. Annotate your domain errors once to surface structured telemetry via std::error::Request and generate conversions into AppError / AppCode.

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));

Use #[provide] to forward typed telemetry that downstream consumers can extract from AppError via std::error::Request.

§Masterror derive: end-to-end domain errors

#[derive(Masterror)] builds on top of #[derive(Error)], wiring a domain error directly into crate::Error with typed telemetry, redaction policy and transport hints. The #[masterror(...)] attribute mirrors the thiserror style while keeping redaction decisions and metadata in one place.

use masterror::{
    AppCode, AppErrorKind, Error, Masterror, MessageEditPolicy, mapping::HttpMapping
};

#[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"
)]
struct MissingFlag {
    user_id: String,
    flag:    &'static str,
    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 — public AppCode.
  • category — semantic AppErrorKind.
  • message — expose the formatted core::fmt::Display output as the public message.
  • redact(message) — mark the message as redactable at the transport boundary, fields("name" = hash, "card" = last4) override metadata policies (hash, last4, redact, none).
  • telemetry(...) — list of expressions producing Option<masterror::Field> to be inserted into Metadata.
  • map.grpc / map.problem — optional gRPC status (as i32) and problem+json type for generated mapping tables. Access them via TYPE::HTTP_MAPPING, TYPE::GRPC_MAPPING/MAPPINGS and TYPE::PROBLEM_MAPPING/MAPPINGS.

The derive continues to honour #[from], #[source] and #[backtrace] field attributes, automatically attaching sources and captured backtraces to the resulting Error.

§Domain integrations: Turnkey

With the turnkey feature enabled, the crate exports a turnkey module that provides:

  • turnkey::TurnkeyErrorKind — stable categories for Turnkey-specific failures
  • turnkey::TurnkeyError — a container with kind and safe, public message
  • turnkey::classify_turnkey_error — heuristic classifier for raw SDK/provider strings
  • conversions: From<TurnkeyError>AppError and From<TurnkeyErrorKind>AppErrorKind

§Example

use masterror::{
    AppError, AppErrorKind,
    turnkey::{TurnkeyError, TurnkeyErrorKind, classify_turnkey_error}
};

// Classify a raw provider message
let kind = classify_turnkey_error("429 Too Many Requests");
assert!(matches!(kind, TurnkeyErrorKind::RateLimited));

// Build and convert into AppError
let e = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "throttled by upstream");
let app: AppError = e.into();
assert_eq!(app.kind, AppErrorKind::RateLimited);

§Error taxonomy

Applications convert domain/infrastructure failures into AppError with a semantic AppErrorKind and optional public message:

use masterror::{AppError, AppErrorKind};

let err = AppError::new(AppErrorKind::BadRequest, "Flag must be set");
assert!(matches!(err.kind, AppErrorKind::BadRequest));

Attach structured metadata for telemetry and logging:

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

let err = AppError::service("downstream degraded")
    .with_field(field::str("request_id", "abc123"))
    .with_field(field::i64("attempt", 2));
assert_eq!(err.metadata().len(), 2);

AppErrorKind controls the default HTTP status mapping.
AppCode provides a stable machine-readable code for clients.
Together, they form the wire contract in ErrorResponse.

§Wire payload: ErrorResponse

The stable JSON payload for HTTP APIs contains:

  • status: u16 — HTTP status code
  • code: AppCode — stable machine-readable error code
  • message: String — human-friendly, safe-to-expose text
  • details — optional details (JSON if serde_json, otherwise string)
  • retry — optional retry advice (Retry-After)
  • www_authenticate — optional authentication challenge

Example construction:

use masterror::{AppCode, ErrorResponse};

let resp = ErrorResponse::new(404, AppCode::NotFound, "User not found").expect("status");

Conversion from AppError:

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

let app_err = AppError::new(AppErrorKind::NotFound, "user_not_found");
let resp: ErrorResponse = (&app_err).into();
assert_eq!(resp.status, 404);
assert!(matches!(resp.code, AppCode::NotFound));

§Typed control-flow macros

Reach for ensure! and fail! when you need to exit early with a typed error without paying for string formatting or heap allocations on the success path.

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

fn guard(flag: bool) -> AppResult<()> {
    masterror::ensure!(flag, AppError::bad_request("flag must be set"));
    Ok(())
}

fn bail() -> 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
));

§Axum integration

With the axum feature enabled, you can return AppError directly from handlers. It is automatically converted into an ErrorResponse JSON payload.

use axum::{routing::get, Router};
use masterror::{AppError, AppResult};

async fn handler() -> AppResult<&'static str> {
    Err(AppError::forbidden("No access"))
}

let app = Router::new().route("/demo", get(handler));

§OpenAPI integration

With the openapi feature enabled, ErrorResponse derives utoipa::ToSchema and can be referenced in OpenAPI operation responses.

§Versioning policy

This crate follows semantic versioning. Any change to the public API or wire contract is considered a breaking change and requires a major version bump.

§Safety

This crate does not use unsafe.

§License

Licensed under either of

  • Apache License, Version 2.0
  • MIT license

at your option.

Modules§

error
Utilities for building custom error derive infrastructure.
field
Factories for Field values.
mapping
Transport mapping descriptors for generated domain errors. Transport mapping descriptors generated by #[derive(Masterror)].
prelude
Minimal prelude re-exporting core types for handler signatures. Minimal, opt-in prelude for application crates.

Macros§

ensure
Abort the enclosing function with an error when a condition fails.
fail
Abort the enclosing function with the provided error.

Structs§

AppError
Backwards-compatible export using the historical name. Rich application error preserving domain code, taxonomy and metadata.
CodeMapping
Canonical mapping for a public AppCode.
Context
Builder describing how to convert an external error into AppError.
Error
Rich application error preserving domain code, taxonomy and metadata.
ErrorResponse
Public, wire-level error payload for HTTP APIs.
Field
Single metadata field – name plus value.
GrpcCode
gRPC status metadata used in RFC7807 payloads and tonic mapping.
Metadata
Structured metadata attached to crate::AppError.
ParseAppCodeError
Error returned when parsing AppCode from a string fails.
ProblemJson
RFC7807 application/problem+json payload enriched with machine-readable metadata.
ProblemMetadata
Metadata section of a ProblemJson payload.
RetryAdvice
Retry advice intended for API clients.

Enums§

AppCode
Stable machine-readable error code exposed to clients.
AppErrorKind
Canonical application error taxonomy.
FieldRedaction
Redaction policy associated with a metadata Field.
FieldValue
Value stored inside Metadata.
MessageEditPolicy
Controls whether the public message may be redacted before exposure.
ProblemMetadataValue
Individual metadata value serialized in problem payloads.

Constants§

CODE_MAPPINGS
Canonical mapping table covering every built-in AppCode.

Traits§

ResultExt
Extension trait for enriching Result errors with Context.

Functions§

mapping_for_code
Lookup helper returning canonical mapping for a given AppCode.

Type Aliases§

AppResult
Conventional result alias for application code.

Derive Macros§

Error
Re-export derive macros so users only depend on this crate.
Masterror
Re-export derive macros so users only depend on this crate.