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 returningAppError
ProblemJson
— 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 toAppError
field
— helper functions to buildMetadata
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
— implementsIntoResponse
forAppError
andProblemJson
with RFC7807 bodyactix
— implementsResponder
forProblemJson
and ActixResponseError
forAppError
tonic
— convertsError
intotonic::Status
with 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::JsValue
and emitconsole.error
logs in WASM/browser contextsserde_json
— support for structured JSON details inErrorResponse
andProblemJson
; also pulled transitively byaxum
multipart
— compatibility flag for Axum multipartturnkey
— domain taxonomy and conversions for Turnkey errors, exposed in theturnkey
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
— publicAppCode
.category
— semanticAppErrorKind
.message
— expose the formattedcore::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 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
/MAPPINGS
andTYPE::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 withkind
and safe, public messageturnkey::classify_turnkey_error
— heuristic classifier for raw SDK/provider strings- conversions:
From<TurnkeyError>
→AppError
andFrom<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 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!(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.
- 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
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
AppCode
from a string fails. - Problem
Json - RFC7807
application/problem+json
payload enriched with machine-readable metadata. - Problem
Metadata - Metadata section of a
ProblemJson
payload. - Retry
Advice - Retry advice intended for API clients.
Enums§
- AppCode
- Stable machine-readable error code exposed to clients.
- AppError
Kind - Canonical application error taxonomy.
- 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.