š« Errors
The greatest mistake is to imagine that we never err. ā Thomas Carlyle.
Errors in Rust are often discussed through two lenses:
Result<T, E>: A control flow type representing success (Ok) or failure (Err).std::error::Error: A trait for values that provide a description and an optional source (cause) chain.
A common point of confusion is that E in a Result is not required to implement the Error trait. In web services, for instance, a middleware might return Result<Response, Response>, where the Err variant is simply an early 403 Forbidden responseānot a āfailureā in the semantic sense.
In Rama, we aim for clarity: If it is an error, it should behave like one. This is particularly important for generic middleware that wraps an unknown service S. If the middleware fails, it needs a consistent way to report that failure, even if it cannot construct the specific error type used by S.
Type Erasure with BoxError
When the specific concrete type of an error matters less than the fact that a failure occurred, Rama uses BoxError.
#![allow(unused)]
fn main() {
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
}
This is a type-erased trait object used at abstraction boundaries. While you can downcast a BoxError to check for a specific type, keep in mind that standard downcasting only inspects the top-level wrapper, not the entire cause chain.
Error Extension (ErrorExt)
The [ErrorExt] trait provides a powerful set of methods to enrich errors. It is implemented for any type that can be converted into a BoxError.
The primary goal of ErrorExt is to add context and diagnostics without losing the original error.
Structured Context
Rama supports a ālogfmtā style of context. When you add context, it is rendered as key-value pairs (or bare values), which are automatically quoted and escaped for log compatibility.
context(value): Adds a bare value to the error.context_field(key, value): Adds a keyed field (e.g.,path="/tmp/logs").- Lazy Variants:
with_contextandwith_context_fieldallow you to provide a closure, ensuring the context is only computed if an error actually occurs.
Backtraces
The .backtrace() method captures a stack trace at the moment of the call.
- Standard Display (
{}): Prints the error and its context. - Alternate Display (
{:#}): Prints the error, the full context list, the cause chain, and the captured backtrace.
Error Context on Containers (ErrorContext)
Often, you want to add context directly to a Result or an Option at the call site. The [ErrorContext] trait enables this:
- **For
Result<T, E>**: Turns the error into a context-enrichedBoxError. - **For
Option<T>**: ConvertsNoneinto aBoxError(starting with the message āOption is Noneā) and attaches your context.
Examples
Option Example:
#![allow(unused)]
fn main() {
use rama_error::ErrorContext;
let value: Option<usize> = None;
// Short-circuit using '?' with enriched info
let value = value.context_field("user_id", 42).context("session missing")?;
}
Result Example:
#![allow(unused)]
fn main() {
use rama_error::{ErrorContext, ErrorExt};
fn perform_io() -> Result<(), std::io::Error> {
/* ... */
Ok(())
}
let res = perform_io()
.context_field("request_id", "abc-123")
.with_context(|| "failed to process storage");
if let Err(err) = res {
// Standard display: "io error | request_id="abc-123" "failed to process storage""
eprintln!("{}", err);
}
}
Error Composition
While BoxError is excellent for high-level reporting, you may want explicit, domain-specific error types for your own logic.
Rama does not force a specific macro-based approach. We recommend defining errors as standard Rust structs or enums and implementing std::error::Error. For background on modern error design, see Sabrina Jewsonās āErrors in Rustā.
For complex HTTP errors, you can explore Ramaās own HTTP rejection macros as inspiration. And, as always, Rama plays well with community standards like thiserror or anyhow if they better fit your workflow.