use core::fmt;
use whereat::{At, ResultAtExt, at};
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(String),
NotFound { key: String },
Validation(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "I/O error: {}", e),
AppError::Parse(msg) => write!(f, "parse error: {}", msg),
AppError::NotFound { key } => write!(f, "not found: {}", key),
AppError::Validation(msg) => write!(f, "validation error: {}", msg),
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
_ => None,
}
}
}
mod good_patterns {
use super::*;
pub fn inner_creates_at() -> Result<String, At<AppError>> {
Err(at(AppError::NotFound {
key: "config".into(),
}))
}
pub fn middle_extends_at() -> Result<String, At<AppError>> {
inner_creates_at().at()
}
pub fn outer_extends_at() -> Result<String, At<AppError>> {
middle_extends_at().at()
}
pub fn with_context() -> Result<String, At<AppError>> {
inner_creates_at().at_str("while loading config")
}
pub fn location_then_context() -> Result<String, At<AppError>> {
inner_creates_at()
.at() .at_str("in outer handler") }
pub fn wrap_external_error() -> Result<String, At<AppError>> {
let io_result: Result<String, std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
io_result
.map_err(|e| at(AppError::Io(e)))
.at_str("reading config file")
}
pub fn wrap_untraced() -> Result<String, At<AppError>> {
fn untraced_library() -> Result<String, AppError> {
Err(AppError::Parse("unexpected token".into()))
}
untraced_library().map_err(at).at_str("parsing user input")
}
pub fn rich_context() -> Result<String, At<AppError>> {
#[allow(dead_code)]
#[derive(Debug)]
struct RequestContext {
user_id: u64,
request_id: String,
}
Err(at(AppError::Validation("invalid email".into()))
.at_str("validating user profile")
.at_debug(|| RequestContext {
user_id: 42,
request_id: "req-123".into(),
})
.at_string(|| format!("at {}", chrono_placeholder())))
}
fn chrono_placeholder() -> &'static str {
"2024-01-15T10:30:00Z"
}
}
mod bad_patterns {
use super::*;
#[allow(dead_code)]
pub fn double_wrapped() -> Result<String, At<At<AppError>>> {
fn inner() -> Result<String, At<AppError>> {
Err(at(AppError::NotFound { key: "x".into() }))
}
Err(at(inner().unwrap_err()))
}
#[allow(dead_code)]
pub fn at_instead_of_at_str() -> Result<String, At<AppError>> {
Err(at(AppError::NotFound { key: "y".into() }))
.at() .at_str("first context")
.at() .at_str("second context")
}
#[allow(dead_code)]
pub fn correct_multiple_contexts() -> Result<String, At<AppError>> {
Err(at(AppError::NotFound { key: "y".into() })
.at_str("first context")
.at_str("second context"))
}
#[allow(dead_code)]
pub fn at_in_tight_loop() -> Result<Vec<i32>, At<AppError>> {
let items = [1, 2, -3, 4, 5];
let mut results = Vec::new();
for item in items {
if item < 0 {
return Err(at(AppError::Validation(format!("negative: {}", item))));
}
results.push(item * 2);
}
Ok(results)
}
#[allow(dead_code)]
pub fn validate_then_error() -> Result<Vec<i32>, At<AppError>> {
let items = [1, 2, -3, 4, 5];
if let Some(bad) = items.iter().find(|&&x| x < 0) {
return Err(at(AppError::Validation(format!("negative: {}", bad))));
}
Ok(items.iter().map(|x| x * 2).collect())
}
}
mod ugly_patterns {
use super::*;
#[allow(dead_code)]
pub fn mixed_error_handling() -> Result<String, At<AppError>> {
fn traced_fn() -> Result<String, At<AppError>> {
Err(at(AppError::NotFound { key: "a".into() }))
}
fn untraced_fn() -> Result<String, AppError> {
Err(AppError::Parse("bad".into()))
}
if let Err(e) = traced_fn() {
return Err(e.at_str("from traced"));
}
if let Err(e) = untraced_fn() {
return Err(at(e).at_str("from untraced"));
}
Ok("ok".into())
}
#[allow(dead_code)]
pub fn conditional_context() -> Result<String, At<AppError>> {
fn inner() -> Result<String, At<AppError>> {
Err(at(AppError::NotFound { key: "z".into() }))
}
inner().map_err(|e| {
match e.error() {
AppError::NotFound { key } => {
let key = key.clone();
e.at_string(move || format!("missing key: {}", key))
}
_ => e.at_str("unknown error"),
}
})
}
}
fn main() {
println!("=== GOOD PATTERNS ===\n");
println!("Pattern 1: Inner creates At, outer extends with .at()");
if let Err(e) = good_patterns::outer_extends_at() {
println!("Error: {}", e);
println!("Debug:\n{:?}\n", e);
}
println!("Pattern 2: Add context with at_str (no new location)");
if let Err(e) = good_patterns::with_context() {
println!("Error: {}", e);
println!("Trace len: {} (should be 1)\n", e.frame_count());
}
println!("Pattern 3: Explicit .at() then .at_str()");
if let Err(e) = good_patterns::location_then_context() {
println!("Error: {}", e);
println!("Trace len: {} (should be 2)\n", e.frame_count());
}
println!("Pattern 4: Converting external errors with start_at");
if let Err(e) = good_patterns::wrap_external_error() {
println!("Error: {}", e);
println!("Debug:\n{:?}\n", e);
}
println!("Pattern 5: Wrapping untraced library errors");
if let Err(e) = good_patterns::wrap_untraced() {
println!("Debug:\n{:?}\n", e);
}
println!("Pattern 6: Multiple contexts on same location");
if let Err(e) = good_patterns::rich_context() {
println!("Error: {}", e);
println!("Trace len: {} (should be 1)", e.frame_count());
println!("Context count: {}\n", e.contexts().count());
println!("Debug:\n{:?}\n", e);
}
println!("\n=== COMPARING GOOD vs BAD ===\n");
println!("Bad: at() when you meant at_str()");
if let Err(e) = bad_patterns::at_instead_of_at_str() {
println!(" Trace len: {} (wasteful!)", e.frame_count());
}
println!("Good: at_str() for context");
if let Err(e) = bad_patterns::correct_multiple_contexts() {
println!(" Trace len: {} (efficient)", e.frame_count());
}
println!("\n=== PERFORMANCE NOTES ===");
println!(
"
- Happy path (no errors): Near-zero overhead (~0.2ns)
- Error creation (at()): ~23ns (dominated by allocation)
- Per context (at_str): ~23ns additional
- Per trace level (.at()): ~6.5ns additional
- Hot loops with 100% errors: 133x slower than plain Result
Guidelines:
1. Use at() at error origin only
2. Use .at() to extend trace at call boundaries
3. Use .at_str() for context (doesn't create new location)
4. Avoid at() in tight loops - validate first, error once
5. Use .map_err(at) when wrapping untraced errors at boundaries
"
);
}