| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
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.
| Crate | What it provides | When to depend on it |
|---|---|---|
| masterror | Core error types, metadata builders, transports, integrations and the prelude. | Application crates, services and libraries that want a stable error surface. |
| masterror-derive | Proc-macros backing #[derive(Error)], #[derive(Masterror)], #[app_error] and #[provide]. | Brought in automatically via masterror; depend directly only for macro hacking. |
| masterror-template | Shared template parser used by the derive macros for formatter analysis. | Internal dependency; reuse when you need the template parser elsewhere. |
Pick only what you need; the default feature set is just std, everything else is opt-in.
The build script keeps the full feature snippet below in sync with Cargo.toml.
[dependencies]
masterror = { version = "0.31.0", default-features = false }
# or with features:
# masterror = { version = "0.31.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"
# ] }Criterion benchmarks cover the hottest conversion paths so regressions are visible before shipping. Run them locally with:
cargo bench -F benchmarks --bench error_pathsThe suite emits two groups:
Adjust Criterion CLI flags (for example --sample-size 200 or --save-baseline local) after -- to trade throughput for tighter confidence intervals when investigating changes.
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 VisualizationsThe inner-most circle represents the entire project, moving outward through folders to individual files. Size and color indicate statement count and coverage percentage.
Each block represents a single file. Block size and color correspond to statement count and coverage percentage.
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.
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::*;
fn do_work(flag: bool) -> AppResult<()> {
if !flag {
return Err(AppError::bad_request("Flag must be set"));
}
Ok(())
}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};
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));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};
fn find(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));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}")]
pub struct DomainError {
#[from]
#[source]
source: io::Error,
}
#[derive(Debug, Error)]
#[error(transparent)]
pub struct WrappedDomainError(
#[from]
#[source]
DomainError
);
fn load() -> 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");#[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"
)]
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)
);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)]
struct TelemetrySnapshot {
name: &'static str,
value: u64,
}
#[derive(Debug, Error)]
#[error("structured telemetry {snapshot:?}")]
#[app_error(kind = AppErrorKind::Service, code = AppCode::Service)]
struct StructuredTelemetryError {
#[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)]
struct OptionalTelemetryError {
#[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)]
enum EnumTelemetryError {
#[error("named {label}")]
#[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound)]
Named {
label: &'static str,
#[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 hintsuse 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");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):
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:
| Mode | Layout |
|---|---|
| Local | Multi-line human-readable report: kind, code, message, source chain, metadata |
| Prod | Compact single-line JSON: kind, code, optional message, metadata |
| Staging | Same 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.31.0", features = ["colored"] }Comprehensive real-world examples demonstrating masterror integration with popular frameworks:
| Example | Description | Features |
|---|---|---|
| axum-rest-api | REST API with RFC 7807 Problem Details | HTTP endpoints, domain errors, integration tests |
| sqlx-database | Database error handling with SQLx | Connection errors, constraint violations, transactions |
| custom-domain-errors | Payment processing domain errors | Derive macro, error conversion, structured errors |
| basic-async | Async error handling with tokio | Error 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.
MSRV: 1.98 · License: MIT · No unsafe
| Back | FazBrowse Home | New Git URL |