Crates.io | thiserror-context |
lib.rs | thiserror-context |
version | |
source | src |
created_at | 2024-07-16 12:38:36.51418 |
updated_at | 2024-09-29 18:41:11.109831 |
description | A wrapper around thiserror, giving you the ability to add context |
homepage | |
repository | https://github.com/tmyers273/error-context |
max_upload_size | |
id | 1305010 |
Cargo.toml error: | TOML parse error at line 18, column 1 | 18 | autolib = false | ^^^^^^^ unknown field `autolib`, expected one of `name`, `version`, `edition`, `authors`, `description`, `readme`, `license`, `repository`, `homepage`, `documentation`, `build`, `resolver`, `links`, `default-run`, `default_dash_run`, `rust-version`, `rust_dash_version`, `rust_version`, `license-file`, `license_dash_file`, `license_file`, `licenseFile`, `license_capital_file`, `forced-target`, `forced_dash_target`, `autobins`, `autotests`, `autoexamples`, `autobenches`, `publish`, `metadata`, `keywords`, `categories`, `exclude`, `include` |
size | 0 |
This library provides a wrapper around a [thiserror] enum, from which you can add additional context, similar to how you would with the [anyhow] crate.
This crate is meant to bridge the gap and provide the best of both worlds between [thiserror] and [anyhow], in that you retain the type of the underlying root error, while allowing you to add additional context to it.
Using [thiserror], you can end up with errors similar to
Sqlx(RowNotFound)
which is not helpful in debugging.
Using [anyhow] gives you much more helpful context:
Sqlx(RowNotFound)
Caused by:
0: loading user id 1
1: authentication
But it comes at the expense of erasing the underlying error type.
This type erasure is problematic in a few ways:
In a happy case, if you remember to convert all your errors into your [thiserror] type, then you can downcast directly to the [thiserror] type.
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(())
}
But, if you forget to convert your error into your [thiserror] type, then things start to get messy.
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(())
}
This crate bridges the two worlds, allowing you to add context to your [thiserror] type while preserving the ergonomics and accessibility of the underlying error enum.
This crate is intended to be used with [thiserror] enums, but should work with any error type.
** Example **
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(())
}
Similar to [context], this crate provides a [Context] trait that extends
the [Result] type with two methods: context
and with_context
.
context adds static context to the error, while with_context adds dynamic context to the error.
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);
Context enriched errors can be nested and they will preserve their context messages when converting from a child error type to a parent.
See [impl_from_carry_context] for more information.