masterror 0.5.0

Application error types and response mapping
Documentation

masterror · Framework-agnostic application error types

Crates.io docs.rs Downloads MSRV License CI

Small, pragmatic error model for API-heavy Rust services. Core is framework-agnostic; integrations are opt-in via feature flags. Stable categories, conservative HTTP mapping, no unsafe.

  • Core types: AppError, AppErrorKind, AppResult, AppCode, ErrorResponse
  • Optional Axum/Actix integration
  • Optional OpenAPI schema (via utoipa)
  • Conversions from sqlx, reqwest, redis, validator, config, tokio

TL;DR

[dependencies]
masterror = { version = "0.5.0", default-features = false }
# or with features:
# masterror = { version = "0.5.0", features = [
#   "axum", "actix", "openapi", "serde_json",
#   "sqlx", "reqwest", "redis", "validator",
#   "config", "tokio", "multipart", "teloxide",
#   "telegram-webapp-sdk", "frontend", "turnkey"
# ] }

Since v0.5.0: derive custom errors via #[derive(Error)] (use masterror::Error;) and inspect browser logging failures with BrowserConsoleError::context(). Since v0.4.0: optional frontend feature for WASM/browser console logging. Since v0.3.0: stable AppCode enum and extended ErrorResponse with retry/authentication metadata.


  • Stable taxonomy. Small set of AppErrorKind categories mapping conservatively to HTTP.
  • Framework-agnostic. No assumptions, no unsafe, MSRV pinned.
  • Opt-in integrations. Zero default features; you enable what you need.
  • Clean wire contract. ErrorResponse { status, code, message, details?, retry?, www_authenticate? }.
  • One log at boundary. Log once with tracing.
  • Less boilerplate. Built-in conversions, compact prelude, and the masterror::Error re-export of thiserror::Error with #[from] / #[error(transparent)] support.
  • Consistent workspace. Same error surface across crates.
[dependencies]
# lean core
masterror = { version = "0.5.0", default-features = false }

# with Axum/Actix + JSON + integrations
# masterror = { version = "0.5.0", features = [
#   "axum", "actix", "openapi", "serde_json",
#   "sqlx", "reqwest", "redis", "validator",
#   "config", "tokio", "multipart", "teloxide",
#   "telegram-webapp-sdk", "frontend", "turnkey"
# ] }

MSRV: 1.89 No unsafe: forbidden by crate.

Create an error:

use masterror::{AppError, AppErrorKind};

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

With prelude:

use masterror::prelude::*;

fn do_work(flag: bool) -> AppResult<()> {
    if !flag {
        return Err(AppError::bad_request("Flag must be set"));
    }
    Ok(())
}
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");
  • use masterror::Error; re-exports thiserror::Error.
  • #[from] automatically implements From<...> while ensuring wrapper shapes are valid.
  • #[error(transparent)] enforces single-field wrappers that forward Display/source to the inner error.
use masterror::{AppError, AppErrorKind, AppCode, ErrorResponse};
use std::time::Duration;

let app_err = AppError::new(AppErrorKind::Unauthorized, "Token expired");
let resp: ErrorResponse = (&app_err).into()
    .with_retry_after_duration(Duration::from_secs(30))
    .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#);

assert_eq!(resp.status, 401);
// features = ["axum", "serde_json"]
...
    assert!(payload.is_object());

    #[cfg(target_arch = "wasm32")]
    {
        if let Err(console_err) = err.log_to_browser_console() {
            eprintln!(
                "failed to log to browser console: {:?}",
                console_err.context()
            );
        }
    }

    Ok(())
}
  • On non-WASM targets log_to_browser_console returns BrowserConsoleError::UnsupportedTarget.
  • BrowserConsoleError::context() exposes optional browser diagnostics for logging/telemetry when console logging fails.
  • axum — IntoResponse integration with structured JSON bodies
  • actix — Actix Web ResponseError and Responder implementations
  • openapi — Generate utoipa OpenAPI schema for ErrorResponse
  • serde_json — Attach structured JSON details to AppError
  • sqlx — Classify sqlx::Error variants into AppError kinds
  • reqwest — Classify reqwest::Error as timeout/network/external API
  • redis — Map redis::RedisError into cache-aware AppError
  • validator — Convert validator::ValidationErrors into validation failures
  • config — Propagate config::ConfigError as configuration issues
  • tokio — Classify tokio::time::error::Elapsed as timeout
  • multipart — Handle axum multipart extraction errors
  • teloxide — Convert teloxide_core::RequestError into domain errors
  • telegram-webapp-sdk — Surface Telegram WebApp validation failures
  • frontend — Log to the browser console and convert to JsValue on WASM
  • turnkey — Ship Turnkey-specific error taxonomy and conversions
  • std::io::Error → Internal
  • String → BadRequest
  • sqlx::Error → NotFound/Database
  • redis::RedisError → Cache
  • reqwest::Error → Timeout/Network/ExternalApi
  • axum::extract::multipart::MultipartError → BadRequest
  • validator::ValidationErrors → Validation
  • config::ConfigError → Config
  • tokio::time::error::Elapsed → Timeout
  • teloxide_core::RequestError → RateLimited/Network/ExternalApi/Deserialization/Internal
  • telegram_webapp_sdk::utils::validate_init_data::ValidationError → TelegramAuth

Minimal core:

masterror = { version = "0.5.0", default-features = false }

API (Axum + JSON + deps):

masterror = { version = "0.5.0", features = [
  "axum", "serde_json", "openapi",
  "sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }

API (Actix + JSON + deps):

masterror = { version = "0.5.0", features = [
  "actix", "serde_json", "openapi",
  "sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
// features = ["turnkey"]
use masterror::turnkey::{classify_turnkey_error, TurnkeyError, TurnkeyErrorKind};
use masterror::{AppError, AppErrorKind};

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

// Wrap into AppError
let e = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "throttled upstream");
let app: AppError = e.into();
assert_eq!(app.kind, AppErrorKind::RateLimited);
  • Use ErrorResponse::new(status, AppCode::..., "msg") instead of legacy
  • New helpers: .with_retry_after_secs, .with_retry_after_duration, .with_www_authenticate
  • ErrorResponse::new_legacy is temporary shim

Semantic versioning. Breaking API/wire contract → major bump. MSRV = 1.89 (may raise in minor, never in patch).

  1. cargo +nightly fmt --
  2. cargo clippy -- -D warnings
  3. cargo test --all
  4. cargo build (regenerates README.md from the template)
  5. cargo doc --no-deps
  6. cargo package --locked
  • Not a general-purpose error aggregator like anyhow
  • Not a replacement for your domain errors

Apache-2.0 OR MIT, at your option.