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 hintsAppErrorKind— stable internal taxonomy of application errorsAppResult— convenience alias for returningAppErrorProblemJson— RFC7807 payload emitted by HTTP/gRPC adaptersErrorResponse— legacy wire-level JSON payload for HTTP APIsAppCode— public, machine-readable error code for clientsMetadata— structured telemetry attached toAppErrorfield— helper functions to buildMetadatawithout manual enums
Key properties:
- Stable, predictable error categories (
AppErrorKind). - Explicit, overridable machine-readable codes (
AppCode). - Structured metadata for observability without ad-hoc
Stringmaps. - 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— implementsIntoResponseforAppErrorandProblemJsonwith RFC7807 bodyactix— implementsResponderforProblemJsonand ActixResponseErrorforAppErrortonic— convertsErrorintotonic::Statuswith sanitized metadataopenapi— derives an OpenAPI schema forErrorResponse(viautoipa)sqlx—From<sqlx::Error>mappingredis—From<redis::RedisError>mappingvalidator—From<validator::ValidationErrors>mappingconfig—From<config::ConfigError>mappingtokio—From<tokio::time::error::Elapsed>mappingreqwest—From<reqwest::Error>mappingteloxide—From<teloxide_core::RequestError>mappingtelegram-webapp-sdk—From<telegram_webapp_sdk::utils::validate_init_data::ValidationError>mappingfrontend— convert errors intowasm_bindgen::JsValueand emitconsole.errorlogs in WASM/browser contextsserde_json— support for structured JSON details inErrorResponseandProblemJson; also pulled transitively byaxummultipart— compatibility flag for Axum multipartturnkey— domain taxonomy and conversions for Turnkey errors, exposed in theturnkeymodule
§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— publicAppCode.category— semanticAppErrorKind.message— expose the formattedcore::fmt::Displayoutput 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 producingOption<masterror::Field>to be inserted intoMetadata.map.grpc/map.problem— optional gRPC status (asi32) and problem+json type for generated mapping tables. Access them viaTYPE::HTTP_MAPPING,TYPE::GRPC_MAPPING/MAPPINGSandTYPE::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 failuresturnkey::TurnkeyError— a container withkindand safe, public messageturnkey::classify_turnkey_error— heuristic classifier for raw SDK/provider strings- conversions:
From<TurnkeyError>→AppErrorandFrom<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);Attach upstream diagnostics without cloning existing Arcs:
use masterror::AppError;
let err = AppError::internal("db down")
.with_context(std::io::Error::new(std::io::ErrorKind::Other, "boom"));
assert!(err.source_ref().is_some());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 codecode: AppCode— stable machine-readable error codemessage: String— human-friendly, safe-to-expose textdetails— optional details (JSON ifserde_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_eq!(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
Fieldvalues. - 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§
- AppCode
- Stable machine-readable error code exposed to clients.
- AppError
- Backwards-compatible export using the historical name. Rich application error preserving domain code, taxonomy and metadata.
- Code
Mapping - 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.
- Error
Chain - Iterator over an error chain, yielding each error in the source sequence.
- Error
Response - Public, wire-level error payload for HTTP APIs.
- Field
- Single metadata field – name plus value.
- Grpc
Code - gRPC status metadata used in RFC7807 payloads and tonic mapping.
- Metadata
- Structured metadata attached to
crate::AppError. - Parse
AppCode Error - Error returned when parsing
AppCodefrom a string fails. - Problem
Json - RFC7807
application/problem+jsonpayload enriched with machine-readable metadata. - Problem
Metadata - Metadata section of a
ProblemJsonpayload. - Retry
Advice - Retry advice intended for API clients.
Enums§
- AppError
Kind - Canonical application error taxonomy.
- Display
Mode - Display mode for error output.
- Field
Redaction - Redaction policy associated with a metadata
Field. - Field
Value - Value stored inside
Metadata. - Message
Edit Policy - Controls whether the public message may be redacted before exposure.
- Problem
Metadata Value - Individual metadata value serialized in problem payloads.
Constants§
- CODE_
MAPPINGS - Canonical mapping table covering every built-in
AppCode.
Traits§
Functions§
- mapping_
for_ code - Lookup helper returning canonical mapping for a given
AppCode.
Type Aliases§
- AppResult
- Conventional result alias for application code.