2 个版本
0.1.1 | 2024年7月16日 |
---|---|
0.1.0 | 2024年7月16日 |
#3 in #anyhow
210 下载/每月
26KB
223 代码行(不含注释)
此库围绕 [thiserror] 枚举提供了一个包装器,允许您添加上下文,类似于使用 [anyhow] crate 的方式。
此 crate 的目的是弥合 [thiserror] 和 [anyhow] 之间的差距,提供两者之间的最佳结合,即在保留底层根错误类型的同时,允许您为其添加额外的上下文。
问题
使用 [thiserror]
使用 [thiserror],您可能会得到类似以下错误
Sqlx(RowNotFound)
这不利于调试。
使用 [anyhow]
使用 [anyhow] 提供了更有用的上下文
Sqlx(RowNotFound)
Caused by:
0: loading user id 1
1: authentication
但代价是抹去了底层错误类型。
这种类型擦除有几个问题
- 如果您想保留使用 [thiserror] 类型的能力,则必须将您所有的错误转换为 [thiserror] 类型。这很容易忘记做,因为 [anyhow] 欣然接受任何错误。
- 如果您忘记将错误转换为 [thiserror] 类型,并且您想要近似匹配 [thiserror] 类型,那么您需要尝试向下转换 [thiserror] 类型的所有可能的变体。反过来,这意味着您需要为您的 [thiserror] 类型中添加的所有新变体添加向下转换尝试。这引入了一个容易忘记做的事情。
在理想情况下,如果您记得将所有错误转换为您的 [thiserror] 类型,那么您可以直接向下转换到 [thiserror] 类型。
use anyhow::Context;
use thiserror::Error;
#[derive(Debug, Error)]
enum ThisError {
#[error("placeholder err")]
Placeholder,
#[error("sqlx err: {0}")]
Sqlx(#[from] sqlx::Error),
}
async fn my_fn() -> anyhow::Result<()> {
async {
// Some db query or something
Err(sqlx::Error::RowNotFound)
}.await
.map_err(ThisError::from) // <-------------- Important!
.context("my_fn")?;
Ok(())
}
async fn caller() -> anyhow::Result<()> {
let r: anyhow::Result<()> = my_fn().await;
if let Err(e) = r {
// So even though we can't match on an anyhow error
// match r {
// Placeholder => { },
// Sqlx(_) => { },
// }
// We can downcast it to a ThisError, then match on that
if let Some(x) = e.downcast_ref::<ThisError>() {
match x {
ThisError::Placeholder => {}
ThisError::Sqlx(_) => {}
}
}
}
Ok(())
}
但是,如果您忘记将错误转换为您的 [thiserror] 类型,那么事情开始变得混乱。
use anyhow::Context;
use thiserror::Error;
#[derive(Debug, Error)]
enum ThisError {
#[error("placeholder err")]
Placeholder,
#[error("sqlx err: {0}")]
Sqlx(#[from] sqlx::Error),
}
async fn my_fn() -> anyhow::Result<()> {
async {
// Some db query or something
Err(sqlx::Error::RowNotFound)
}.await
.context("my_fn")?; // <----------- No intermediary conversion into ThisError
Ok(())
}
async fn caller() -> anyhow::Result<()> {
let r: anyhow::Result<()> = my_fn().await;
if let Err(e) = r {
// We still can't match on an anyhow error
// match r {
// Placeholder => { },
// Sqlx(_) => { },
// }
if let Some(x) = e.downcast_ref::<ThisError>() {
// We forgot to explicitly convert our error,
// so this will never run
unreachable!("This will never run");
}
// So, to be safe, we can start attempting to downcast
// all the error types that `ThisError` supports?
if let Some(x) = e.downcast_ref::<sqlx::Error>() {
// That's okay if ThisError is relatively small,
// but it's error prone in that we have to remember
// to add another downcast attempt for any new
// error variants that are added to `ThisError`
}
}
Ok(())
}
解决方案
此 crate 弥合了这两个世界,允许您在保留底层错误枚举的易用性和可访问性的同时,为您的 [thiserror] 类型添加上下文。
此 crate 旨在与 [thiserror] 枚举一起使用,但应与任何错误类型一起工作。
** 示例 **
use thiserror::Error;
use error_context::{Context, impl_context};
// A normal, run-of-the-mill thiserror enum
#[derive(Debug, Error)]
enum ThisErrorInner {
#[error("placeholder err")]
Placeholder,
#[error("sqlx err: {0}")]
Sqlx(#[from] sqlx::Error),
}
// Defines a new type, `ThisErr`, that wraps `ThisErrorInner` and allows
// additional context to be added.
impl_context!(ThisError(ThisErrorInner));
// We are returning the wrapped new type, `ThisError`, instead of the
// underlying `ThisErrorInner`.
async fn my_fn() -> Result<(), ThisError> {
async {
// Some db query or something
Err(sqlx::Error::RowNotFound)
}.await
.context("my_fn")?;
Ok(())
}
async fn caller() -> anyhow::Result<()> {
let r: Result<(), ThisError> = my_fn().await;
if let Err(e) = r {
// We can now match on the error type!
match e.as_ref() {
ThisErrorInner::Placeholder => {}
ThisErrorInner::Sqlx(_) => {}
}
}
Ok(())
}
使用方法
类似于 [context],此 crate 提供了一个 [Context] trait,它扩展了 [Result] 类型,并添加了两个方法: context
和 with_context
。
context 向错误添加静态上下文,而 with_context 向错误添加动态上下文。
use thiserror::Error;
use error_context::{Context, impl_context};
#[derive(Debug, Error)]
enum ThisErrorInner {
#[error("placeholder err")]
Placeholder,
}
impl_context!(ThisError(ThisErrorInner));
fn f(id: i64) -> Result<(), ThisError> {
Err(ThisErrorInner::Placeholder.into())
}
fn t(id: i64) -> Result<(), ThisError> {
f(id)
.context("some static context")
.with_context(|| format!("for id {}", id))
}
let res = t(1);
assert!(res.is_err());
let err = res.unwrap_err();
let debug_repr = format!("{:#?}", err);
assert_eq!(r#"Placeholder
Caused by:
0: for id 1
1: some static context
"#, debug_repr);
嵌套
丰富上下文的错误可以嵌套,并且它们在从子错误类型转换为父错误类型时将保留其上下文消息。
有关更多信息,请参阅 [impl_from_carry_context]。