Scoped Error in Rust

Lobsters Hottest Tools

Summary

Kan-Ru Chen introduces `scoped-error`, a new Rust crate that aims to improve error handling ergonomics by scoping context attachment to the module level, addressing issues with existing crates like anyhow and thiserror.

<p><a href="https://lobste.rs/s/kmbmt1/scoped_error_rust">Comments</a></p>
Original Article
View Cached Full Text

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()) } ```

Similar Articles

A Novel Look at Error Handling in Rust

Lobsters Hottest

The article discusses different error handling patterns in Rust, including panicking, using Option, Result, and default recovery, and proposes a novel approach to error handling beyond simple propagation or recovery.

Custom Errors Are Non-Negotiable in My Rust Applications

Lobsters Hottest

A blog post advocating for custom error types in Rust applications, explaining how to create a unified AppError enum using map_err and From traits to streamline error handling across different subsystems.

Work In Progress Rust

Lobsters Hottest

This article presents techniques and a library for deferring error handling in Rust during development, allowing developers to temporarily downgrade errors to warnings to maintain productivity without sacrificing correctness.

Rust: Don't Panic

Lobsters Hottest

A video transcript discussing how to avoid panics in Rust by using combinators and the Result type instead of unwrap, explaining the three typical crash causes and emphasizing writing robust code.

Safe Made Easy Pt.1: Single Ownership is (Not) Optional

Lobsters Hottest

This article introduces a new approach to memory safety based on linear types and abstract interpretation, aiming to eliminate common bugs like use-after-free and memory leaks more ergonomically than Rust.