以用户友好的方式在任务边界之间传播错误
我在写一个小型的命令行应用来学习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::Errorreqwest::ErrorPoisonError<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 forBox<dyn StdError + std::marker::Send>
我的问题是:怎样以高效的方式把这些类型转换为某种 dyn StdError + Send?或者对于异步程序,是否有其他好用的错误传播模式?
我发现了这些问题,但我不认为它们是重复的:
- How do I propagate errors from tokio::spawn's future? 这并非太多关于类型的问题,更多像是在问线程之间的通信
- How to propagate errors from multiple threads in rust? 更像是在谈论panic与 Result的对比,而不是关于拥有
Sendable错误类型这一具体问题
解决方案
通常对于非 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());
}
为什么会这样?因为标准库具有以下实现:
impl<'a> From<&str> for Box<dyn Error + 'a>impl<'a> From<&str> for Box<dyn Error + Sync + Send + 'a>impl<'a, E> From<E> for Box<dyn Error + 'a> where E: Error + 'aimpl<'a, E> From<E> for Box<dyn Error + Sync + Send + 'a> where E: Error + Send + Sync + 'a
但它并没有为 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!()
}