Cached at:
05/25/26, 07:06 AM
# Scoped Error in Rust
Source: [https://kanru.info/scoped-error/](https://kanru.info/scoped-error/)
西元2026年05月22日 \- Kan\-Ru Chen- [The Inspiration](https://kanru.info/scoped-error/#the-inspiration)
- [Enter Scoped Error](https://kanru.info/scoped-error/#enter-scoped-error)
I’ve never been fully satisfied with any error handling crate in Rust\. I’ve tried many and even developed a few helpers\. Here are the key issues I found with each\. Theses are issues my`scoped\-error`crate tries to address\.
## The Inspiration
**`anyhow`**\- good for a drop\-in Error type that just works, but requires adding`\.with\_context\(\)`everywhere\. It’s verbose and repetitive\. Error reporting requires knowing how`anyhow::Error`handles format strings\. Error propagation lacks location information; the alternative is backtrace, which pulls in heavy std dependencies\.
**`thiserror`**\- good for defining custom Error types\. The`\#\[from\]`implementation encourages a single Error type that encompasses all possible sources\. But the ergonomics stop there\. Using these types is still tedious if you want per\-module errors with good context\. The improvement over manually rolling Error types seems small compared to the`syn`and compile\-time overhead\.
**`snafu`**\- combines manual context attachment with`anyhow`and`thiserror`patterns in one crate\. However, I feel like I’m encoding all my error branches into`Snafu`contexts\. Those implementation details don’t need to be public, yet`snafu`tightly couples the Error type to them\. Maybe I’m using it wrong\.
**`exn`**\- a refreshing approach to error handling\. I actually started my crate based on the pattern from the blog post[Stop Forwarding Errors, Start Designing Them](https://fast.github.io/blog/stop-forwarding-errors-start-designing-them/)\. The minor issues with`exn`0\.3 are: \(1\) you still need to remember`\.or\_raise\(err\)`for each fallible operation, and it’s easy to miss for intra\-module method calls; \(2\) the`Exn`wrapper itself is not a std`Error`, so interop with other error types requires adapters like`exn\-anyhow`or`exn\-stderr`\.
While switching between these error crates, I kept noticing a gap: with`anyhow`\-like crates, you attach context at each call site, but the method itself lacks it\.
Example:
```
use anyhow::Result;
fn read_config() -> Result<String> {
let raw = std::fs::read_to_string("config.toml")?;
Ok(raw)
}
fn complex_method() -> Result<()> {
let cfg = read_config().context("validate config file")?;
parse(cfg).context("parse config file")?;
Ok(())
}
```
It’s easy to`?`away the context when all your methods return`anyhow::Result`\.
With`exn`\-like crates, you define error context for each method and attach it to all error branches\.`snafu`works similarly\.
Example:
```
use exn::{Result, ResultExt};
use thiserror::Error;
#[derive(Debug, Error)]
#[error("MyError: {0}")]
struct MyError(&'static str);
fn read_config() -> Result<String, MyError> {
let err = || MyError("read config file");
let raw = std::fs::read_to_string("config.toml").or_raise(err)?;
Ok(raw)
}
fn complex_method() -> Result<(), MyError> {
let err = || MyError("complex method");
let cfg = read_config().or_raise(err)?;
parse(cfg).or_raise(err)?;
Ok(())
}
```
The common issue: it’s easy to`?`away the context when all methods share the same`Result`type\.
## Enter Scoped Error
I really liked`exn`’s approach: define an error closure to force conversion to the module\-scoped Error type\. But the repeated`\.or\_raise\(err\)?`gets annoying fast\.
I started creating wrappers to mediate conversion from source errors to module\-scoped Errors\. I soon realized this pattern solves several ergonomic issues with other approaches, and checks the boxes I cared about\.
Example:
```
use scoped_error::{Error, expect_error};
fn read_config() -> Result<String, Error> {
expect_error("read config file", || {
let raw = std::fs::read_to_string("config.toml")?;
Ok(raw)
})
}
fn complex_method() -> Result<(), Error> {
expect_error("failed to do complex thing", || {
let cfg = read_config()?;
parse(cfg)?;
Ok(())
})
}
```
The core idea is simple: attach context exactly once\. Not at every call site, not at every failure point, just between caller and logic\. I want per\-module Error types without manual conversion at every step\.
`expect\_error\(\)`has three responsibilities: prepare context for future errors, force inner errors into a boxed type to type\-erase the inner error, and wrap the outer error with the inner as its source\.
The result: a clean, readable declaration of fallible operations\. A default`Error`type is provided, but any std`Error`implementing`WithContext`works too\.
The core library is tiny\. It’s small enough to vendor directly into your project[1](https://kanru.info/scoped-error/#1)\.
The inner boxed error type`Frame`takes its name from`exn`\. It converts any error to`Box<dyn Error\>`and captures file location via`\#\[track\_caller\]`for a lightweight stack trace\.
With the built\-in`Error`type or the`ErrorExt::report\(\)`helper, error trees \(yes, trees are supported\) render like this:
```
Error: failed to do complex thing, at src/main.rs:12:19
|-- read config file, at src/main.rs:5:19
`-- No such file or directory (os error 2)
```
The[`scoped\-error`](https://crates.io/crates/scoped-error)crate also packs a few extras: a`macro\_rules\!`macro for creating common errors that implement`WithContext`, and a`Many`error type for multi\-cause errors\.
The crate is on crates\.io, source is on[Codeberg](https://codeberg.org/kanru/scoped-error), and[docs](https://docs.rs/scoped-error/)cover the details\. If you try it, I’d love to hear your use cases\. File an issue or drop me a note\. I’m using this in my own projects now and finally Rust error handling feels right\.
```
// Copyright (C) 2026 Kan-Ru Chen <
[email protected]>
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
use std::any::Any;
use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt::{Debug, Display};
use std::panic::Location;
/// A trait for error types that can carry context information.
pub trait WithContext: StdError + Any {
/// Attach a context layer to this error.
fn with_context(self, context: Frame) -> Self;
/// Get the location where this error was created or where context
/// was attached.
fn location(&self) -> Option<&'static Location<'static>>;
}
/// A single layer of error context.
pub struct Frame {
/// The underlying error that caused this context.
pub source: Box<dyn StdError + Send + Sync + 'static>,
/// The location where this context was attached.
pub location: &'static Location<'static>,
}
impl<T> From<T> for Frame
where
T: Into<Box<dyn StdError + Send + Sync + 'static>>,
{
/// Creates a `Frame` from any error type, capturing the
/// caller's location.
#[track_caller]
fn from(value: T) -> Self {
let source = value.into();
let location = Location::caller();
Frame { source, location }
}
}
/// Low-level function for adding context with a custom error constructor.
#[inline(always)]
pub fn expect_error_fn<F, T, E>(
err: F,
body: impl FnOnce() -> Result<T, Frame>,
) -> Result<T, E>
where
F: FnOnce() -> E,
E: WithContext,
{
body().map_err(|context| err().with_context(context))
}
/// Add context to errors, returning a custom error type.
#[inline(always)]
pub fn expect_error<T, E>(
msg: impl Into<Cow<'static, str>>,
body: impl FnOnce() -> Result<T, Frame>,
) -> Result<T, E>
where
E: From<(Cow<'static, str>, Frame)>,
{
body().map_err(|context| (msg.into(), context).into())
}
```