#error-context #thiserror #error #context #anyhow

thiserror-context

A wrapper around thiserror, giving you the ability to add context

2 个版本

0.1.1 2024年7月16日
0.1.0 2024年7月16日

#3 in #anyhow

Download history 186/week @ 2024-07-13 26/week @ 2024-07-20 38/week @ 2024-07-27 32/week @ 2024-08-03 63/week @ 2024-08-10 76/week @ 2024-08-17

210 下载/每月

MIT/Apache

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] 类型,并添加了两个方法: contextwith_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]。

无运行时依赖