以用户友好的方式在任务边界之间传播错误

编程语言 2026-07-11

我在写一个小型的命令行应用来学习Rust,并且在错误传播方面使用了大量 Result<_, Box<dyn std::error::Error>>。这相当灵活,运作也不错。

现在我尝试在程序中引入async/await代码。首先要明白的是 std::error::Error 并不意味着 Send。但我的程序需要在任务边界之间进行一些错误传播,例如当我执行 tokio::spawn(foo()) 时。因此,朴素的解决方案是在需要的地方把返回类型约束为 Result<_, Box<dyn std::error::Error + Send>>

不过现在我遇到了一些类型,不能自动被强制转换为 dyn std::error::Error + Send。具体来说,以下情况在没有 Send 的情况下能工作,但在使用 Send 时就不行:

  • Err("something".into())
  • std::io::Error
  • reqwest::Error
  • PoisonError<std::sync::MutexGuard<'_, _>>

看这个示例函数:

async fn foo() -> Result<(), Box<dyn std::error::Error>> {
    let text = reqwest::get("https://example.com/").await?.text().await?;
    println!("{text}");
    Err("example".into())
}

如果我把 dyn Error+ Send 进行约束,那么两个问号运算符和 .into() 都无法编译。

确切的错误信息是(或对另一种类型缺少 From):

the trait From<&str> is not implemented for Box<dyn StdError + std::marker::Send>

我的问题是:怎样以高效的方式把这些类型转换为某种 dyn StdError + Send?或者对于异步程序,是否有其他好用的错误传播模式?


我发现了这些问题,但我不认为它们是重复的:

解决方案

通常对于非 Send 错误你会看到 Box<dyn Error>,否则你会看到用于跨线程边界的盒装错误的 Box<dyn Error + Send + Sync>。如果你添加缺失的 Sync 上界,所有这些转换都会成功(除了因为它包含的生命周期而导致的 PoisonError,正如评论中所指出的,因为它未通过隐含的 'static 上界)。

下面的代码可以正常编译。把 Sync 上界从 require_box_error 的签名中移除,你将看到你所遇到的相同错误。

#![allow(unused)]

fn io_err() -> std::io::Error {
    todo!()
}

fn reqwest_err() -> reqwest::Error {
    todo!()
}

fn require_box_error(_: Box<dyn std::error::Error + Send + Sync>) {}

fn test() {
    require_box_error("foo".into());
    require_box_error(io_err().into());
    require_box_error(reqwest_err().into());
}

(Playground)

为什么会这样?因为标准库具有以下实现:

但它并没有为 Box<dyn Error + Send + 'a> 提供类似的实现。


要处理 PoisonError 情况,你可以把它映射为一个 PoisonError<()>,它不包含生命周期,因此可以被盒装进一个trait对象。错误信息(也就是你真正关心的部分)不会受到内部值的影响。

fn poison_example<T>(
    mutex: &std::sync::Mutex<T>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut guard = mutex.lock().map_err(|_| std::sync::PoisonError::new(()))?;

    todo!()
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章