Error Forge is a Rust error-handling crate built around a few simple ideas:
- Errors should carry stable metadata such as kind, retryability, status code, and exit code.
- Application code should be able to add context without destroying the original cause chain.
- Operational tooling should have clear hooks for logging, formatting, codes, and recovery policies.
- Feature-gated integrations should stay optional so the core remains lightweight.
It ships with a built-in AppError, a declarative define_errors! macro, an optional #[derive(ModError)] proc macro, error collectors, registry support, console formatting, synchronous retry and circuit-breaker primitives, and async-specific traits behind feature flags.
[dependencies]
error-forge = "0.9.7"Common optional features:
derive: enables#[derive(ModError)]async: enablesAsyncForgeErrorserde: enables serialization support where compatiblelog: enables thelogadaptertracing: enables thetracingadapter
use error_forge::{AppError, ForgeError};
fn load_config() -> Result<(), AppError> {
Err(AppError::config("Missing DATABASE_URL").with_fatal(true))
}
fn main() {
let error = load_config().unwrap_err();
assert_eq!(error.kind(), "Config");
assert!(error.is_fatal());
println!("{}", error);
}define_errors! is the lowest-friction way to create a custom error enum with generated constructors and ForgeError metadata.
use error_forge::{define_errors, ForgeError};
use std::io;
define_errors! {
pub enum ServiceError {
#[error(display = "Configuration is invalid: {message}", message)]
#[kind(Config, status = 500)]
Config { message: String },
#[error(display = "Request to {endpoint} failed", endpoint)]
#[kind(Network, retryable = true, status = 503)]
Network { endpoint: String, source: Option<Box<dyn std::error::Error + Send + Sync>> },
#[error(display = "Could not read {path}", path)]
#[kind(Filesystem, status = 500)]
Filesystem { path: String, source: io::Error },
}
}
fn main() {
let error = ServiceError::config("Missing API token".to_string());
assert_eq!(error.kind(), "Config");
assert_eq!(error.status_code(), 500);
}Notes:
#[kind(...)]is required for each variant.- Constructors are generated from the lowercase variant name, such as
ServiceError::config(...). - A field named
sourceparticipates instd::error::Error::source()chaining. - For custom
sourcefield types, implementerror_forge::macros::ErrorSourcein your crate. - With the
serdefeature enabled, source fields must themselves be serializable if you want to derive serialization through the macro-generated enum.
use error_forge::{AppError, ResultExt};
fn connect() -> Result<(), AppError> {
Err(AppError::network("db.internal", None))
}
fn main() {
let error = connect()
.with_context(|| "opening primary database connection".to_string())
.unwrap_err();
println!("{}", error);
}use error_forge::{AppError, ErrorCollector};
fn main() {
let mut collector = ErrorCollector::new();
collector.push(AppError::config("missing host"));
collector.push(AppError::other("invalid timeout"));
assert_eq!(collector.len(), 2);
println!("{}", collector.summary());
}Enable the derive feature to use #[derive(ModError)].
use error_forge::{ForgeError, ModError};
#[derive(Debug, ModError)]
#[error_prefix("Database")]
enum DbError {
#[error_display("Connection failed: {0}")]
#[error_retryable]
#[error_http_status(503)]
ConnectionFailed(String),
#[error_display("Query failed for {query}")]
QueryFailed { query: String },
#[error_display("Permission denied")]
#[error_fatal]
PermissionDenied,
}
fn main() {
let error = DbError::ConnectionFailed("primary".to_string());
assert!(error.is_retryable());
assert_eq!(error.status_code(), 503);
}Supported derive attributes:
error_prefixerror_displayerror_kinderror_captionerror_retryableerror_http_statuserror_exit_codeerror_fatal
Both list-style and name-value forms are supported for error_prefix.
The recovery module is intentionally synchronous today. It is designed for blocking code, worker threads, and service wrappers where a small sleep is acceptable.
use error_forge::recovery::{CircuitBreaker, RetryPolicy};
fn main() {
let breaker = CircuitBreaker::new("inventory-service");
let policy = RetryPolicy::new_fixed(25).with_max_retries(3);
let value: Result<u32, std::io::Error> = breaker.execute(|| {
policy.retry(|| Ok(42))
});
assert_eq!(value.unwrap(), 42);
}If you need async retries, keep Error Forge for modeling and classification, then wrap retry behavior with your async runtime of choice.
use error_forge::{
AppError,
macros::{try_register_error_hook, ErrorLevel},
};
fn main() {
let _ = try_register_error_hook(|ctx| {
if matches!(ctx.level, ErrorLevel::Critical | ErrorLevel::Error) {
eprintln!("{} [{}]", ctx.caption, ctx.kind);
}
});
let _ = AppError::config("Missing environment variable");
}logging::register_logger(...)installs a custom logger once.logging::log_impl::init()is available with thelogfeature.logging::tracing_impl::init()is available with thetracingfeature.
use error_forge::{console_theme::print_error, AppError};
fn main() {
let error = AppError::filesystem("config.toml", None);
print_error(&error);
}Attach stable codes to errors when you want machine-readable identifiers or documentation links.
use error_forge::{register_error_code, AppError, ForgeError};
fn main() {
let _ = register_error_code(
"AUTH-001",
"Authentication failed",
Some("https://example.com/errors/AUTH-001"),
false,
);
let error = AppError::config("Invalid credentials")
.with_code("AUTH-001")
.with_status(401);
assert_eq!(error.status_code(), 401);
println!("{}", error.dev_message());
}The crate is validated with:
cargo test --all-featurescargo clippy --all-targets --all-features -- -D warnings- targeted examples and feature-gated regression coverage
- API reference:
docs/API.md - Examples:
examples/ - Crate documentation: https://docs.rs/error-forge
Licensed under Apache-2.0.