Rust中的作用域错误

Lobsters Hottest 工具

摘要

Kan-Ru Chen 介绍了 `scoped-error`,一个新的 Rust crate,旨在通过将上下文附件限定在模块级别来改善错误处理的人机工学,解决了像 anyhow 和 thiserror 等现有 crate 的问题。

<p><a href="https://lobste.rs/s/kmbmt1/scoped_error_rust">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/25 07:06

# Rust 中的作用域错误(Scoped Error) 来源:https://kanru.info/scoped-error/ 西元2026年05月22日 \- Kan\-Ru Chen - [灵感来源](#the-inspiration) - [进入作用域错误](#enter-scoped-error) 我从未对 Rust 中的任何错误处理 crate 完全满意过。我尝试过很多,甚至自己开发了一些辅助工具。以下是我发现的每个 crate 的关键问题,而这些问题正是我的 `scoped-error` crate 试图解决的。 ## 灵感来源 **`anyhow`** 适用于即插即用的 Error 类型,开箱即用,但需要在每个地方添加 `.with_context()`,显得冗长且重复。错误报告需要了解 `anyhow::Error` 如何处理格式化字符串。错误传播缺少位置信息;替代方案是 backtrace,但这会引入较重的 std 依赖。 **`thiserror`** 适用于定义自定义 Error 类型。`#[from]` 实现鼓励使用一个单一的 Error 类型来涵盖所有可能的来源。但人体工程学止步于此。如果你想获得带有良好上下文的每个模块错误,使用这些类型仍然很繁琐。与手动编写 Error 类型相比,它的改进似乎很小,却要付出 `syn` 和编译时开销的代价。 **`snafu`** 将手动上下文附加与 `anyhow` 和 `thiserror` 模式结合在一个 crate 中。然而,我感觉自己正在将所有错误分支编码成 `Snafu` 上下文。这些实现细节本不需要公开,但 `snafu` 将 Error 类型与它们紧密耦合。也许是我用错了。 **`exn`** 一种令人耳目一新的错误处理方法。我实际上是根据博客文章《停止转发错误,开始设计它们》的模式开始编写我的 crate 的。`exn` 0.3 的小问题是:(1) 你仍然需要为每个可失败操作记住 `.or_raise(err)`,而且很容易在模块内部方法调用中漏掉;(2) `Exn` 包装器本身不是 std `Error`,因此与其他错误类型的互操作需要像 `exn-anyhow` 或 `exn-stderr` 这样的适配器。 在切换这些错误 crate 的过程中,我不断注意到一个差距:使用 `anyhow` 类的 crate,你在每个调用点附加上下文,但方法本身却缺少上下文。示例: ```rust 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(()) } ``` 当所有方法都返回 `anyhow::Result` 时,很容易用 `?` 把上下文弄丢。使用 `exn` 类的 crate,你为每个方法定义错误上下文,并将其附加到所有错误分支上。`snafu` 的工作方式类似。示例: ```rust 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(()) } ``` 共同问题是:当所有方法共享同一个 `Result` 类型时,很容易用 `?` 把上下文弄丢。 ## 进入作用域错误 我非常喜欢 `exn` 的方法:定义一个错误闭包来强制转换为模块作用域的 Error 类型。但重复的 `.or_raise(err)?` 很快就会让人厌烦。我开始创建包装器来调解从源错误到模块作用域 Error 的转换。很快我意识到这种模式解决了其他方法的一些人体工程学问题,并满足了我关心的所有条件。示例: ```rust 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(()) }) } ``` 核心思想很简单:只附加一次上下文。不是每个调用点,不是每个失败点,而是在调用者和逻辑之间。我希望每个模块都有 Error 类型,而不必在每个步骤手动转换。 `expect_error()` 有三个职责:准备未来错误的上下文,将内部错误强制转换为盒装类型以消除内部错误的类型,以及用内部错误作为源包装外部错误。结果是:一个干净、可读的可失败操作声明。默认提供了一个 `Error` 类型,但任何实现了 `WithContext` 的 std `Error` 都可以。核心库非常小,小到可以直接 vendored 到你的项目中¹。 内部的盒装错误类型 `Frame` 取名自 `exn`。它将任何错误转换为 `Box<dyn StdError + Send + Sync + 'static>`,并通过 `#[track_caller]` 捕获文件位置,提供轻量级的堆栈跟踪。使用内置的 `Error` 类型或 `ErrorExt::report()` 辅助方法,错误树(是的,树是支持的)会显示如下: ``` 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) ``` `scoped-error` ² crate 还附带了一些额外功能:一个用于创建实现了 `WithContext` 的常见错误的 `macro_rules!` 宏,以及一个用于多原因错误的 `Many` 类型。该 crate 发布在 crates.io 上,源代码在 Codeberg ³,文档 ⁴ 涵盖了详细信息。 如果你尝试了,我很想听听你的用例。可以提 issue 或给我留言。我现在在自己项目中使用它,终于感觉 Rust 的错误处理对了。 ```rust // Copyright (C) 2026 Kan-Ru Chen // 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; /// 一个可以为错误携带上下文信息的 trait。 pub trait WithContext: StdError + Any { /// 向此错误附加一个上下文层。 fn with_context(self, context: Frame) -> Self; /// 获取创建此错误或附加上下文的位置。 fn location(&self) -> Option<&'static Location<'static>>; } /// 错误上下文的单个层。 pub struct Frame { /// 导致此上下文的底层错误。 pub source: Box<dyn StdError + Send + Sync + 'static>, /// 附加此上下文的位置。 pub location: &'static Location<'static>, } impl<T> From<T> for Frame where T: Into<Box<dyn StdError + Send + Sync + 'static>>, { /// 从任何错误类型创建 `Frame`,并捕获调用者的位置。 #[track_caller] fn from(value: T) -> Self { let source = value.into(); let location = Location::caller(); Frame { source, location } } } /// 低级函数,用于使用自定义错误构造函数添加上下文。 #[inline(always)] pub fn expect_error_fn<E, F, T>( err: F, body: impl FnOnce() -> Result<T, E>, ) -> Result<T, E> where F: FnOnce() -> E, E: WithContext, { body().map_err(|context| err().with_context(context)) } /// 为错误添加上下文,返回自定义错误类型。 #[inline(always)] pub fn expect_error<M, T, E>( msg: M, body: impl FnOnce() -> Result<T, E>, ) -> Result<T, E> where M: Into<Cow<'static, str>>, E: From<(Cow<'static, str>, Frame)>, { body().map_err(|context| (msg.into(), context).into()) } ``` --- ¹ https://kanru.info/scoped-error/#1 ² https://crates.io/crates/scoped-error ³ https://codeberg.org/kanru/scoped-error ⁴ https://docs.rs/scoped-error/

相似文章

擦除存在类型

Lobsters Hottest

深入探讨 Rust 类型系统中的存在量词,比较 `dyn Trait` 和 `impl Trait`,并探索超越 `Self` 的存在量化类型变量的高级模式。

安全 Rust 的边界

Lobsters Hottest

TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。

最小可行的Zig错误上下文

matklad

一篇博文,详细介绍了使用errdefer日志在Zig中添加错误上下文的最小模式,并将其与完整的诊断接收器(diagnostics sinks)和catch块进行比较,讨论了权衡取舍。