thiserror_context/
lib.rs

1//! This library provides a wrapper around a [thiserror] enum, from which
2//! you can add additional context, similar to how you would with the [anyhow]
3//! crate.
4//!
5//! This crate is meant to bridge the gap and provide the best of both worlds between
6//! [thiserror] and [anyhow], in that you retain the type of the underlying root error,
7//! while allowing you to add additional context to it.
8//!
9//! # Problem
10//!
11//! ## With [thiserror]
12//!
13//! Using [thiserror], you can end up with errors similar to
14//! ```text
15//! Sqlx(RowNotFound)
16//! ```
17//! which is not helpful in debugging.
18//!
19//! ## With [anyhow]
20//!
21//! Using [anyhow] gives you much more helpful context:
22//!
23//! ```text ignore
24//! Sqlx(RowNotFound)
25//!
26//! Caused by:
27//!   0: loading user id 1
28//!   1: authentication
29//! ```
30//!
31//! But it comes at the expense of erasing the underlying error type.
32//!
33//! This type erasure is problematic in a few ways:
34//! - if you want to preserve the ability to use your [thiserror] type, it
35//!   forces you to convert all your errors in your [thiserror] type. This
36//!   is particularly easy to forget to do, as [anyhow] will happily accept
37//!   any error.
38//! - if you forget to convert an error into your [thiserror] type and you want
39//!   to have something approximating the match on your [thiserror] type, then
40//!   you need to attempt to downcast all the possible variants of the [thiserror]
41//!   type. In turn, that means you need to add a downcast attempt for any new
42//!   variants you add to your [thiserror] type. This introduces an easy thing
43//!   to forget to do.
44//!
45//! In a happy case, _if_ you remember to convert _all_ your errors into your [thiserror]
46//! type, then you can downcast directly to the [thiserror] type.
47//!
48//! ```
49//! use anyhow::Context;
50//! use thiserror::Error;
51//!
52//! #[derive(Debug, Error)]
53//! enum ThisError {
54//!     #[error("placeholder err")]
55//!     Placeholder,
56//!
57//!     #[error("sqlx err: {0}")]
58//!     Sqlx(#[from] sqlx::Error),
59//! }
60//!
61//! async fn my_fn() -> anyhow::Result<()> {
62//!     async {
63//!         // Some db query or something
64//!        Err(sqlx::Error::RowNotFound)
65//!     }.await
66//!         .map_err(ThisError::from) // <-------------- Important!
67//!         .context("my_fn")?;
68//!     Ok(())
69//! }
70//!
71//! async fn caller() -> anyhow::Result<()> {
72//!     let r: anyhow::Result<()> = my_fn().await;
73//!
74//!     if let Err(e) = r {
75//!         // So even though we can't match on an anyhow error
76//!         // match r {
77//!         //     Placeholder => { },
78//!         //     Sqlx(_) => { },
79//!         // }
80//!
81//!         // We can downcast it to a ThisError, then match on that
82//!         if let Some(x) = e.downcast_ref::<ThisError>() {
83//!             match x {
84//!                 ThisError::Placeholder => {},
85//!                 ThisError::Sqlx(_) => {},
86//!             }
87//!         }
88//!     }
89//!
90//!     Ok(())
91//! }
92//! ```
93//!
94//! But, if you forget to convert your error into your [thiserror] type,
95//! then things start to get messy.
96//!
97//! ```
98//! use anyhow::Context;
99//! use thiserror::Error;
100//!
101//! #[derive(Debug, Error)]
102//! enum ThisError {
103//!     #[error("placeholder err")]
104//!     Placeholder,
105//!
106//!     #[error("sqlx err: {0}")]
107//!     Sqlx(#[from] sqlx::Error),
108//! }
109//!
110//! async fn my_fn() -> anyhow::Result<()> {
111//!     async {
112//!         // Some db query or something
113//!        Err(sqlx::Error::RowNotFound)
114//!     }.await
115//!         .context("my_fn")?; // <----------- No intermediary conversion into ThisError
116//!     Ok(())
117//! }
118//!
119//! async fn caller() -> anyhow::Result<()> {
120//!     let r: anyhow::Result<()> = my_fn().await;
121//!
122//!     if let Err(e) = r {
123//!         // We still can't match on an anyhow error
124//!         // match r {
125//!         //     Placeholder => { },
126//!         //     Sqlx(_) => { },
127//!         // }
128//!
129//!         if let Some(x) = e.downcast_ref::<ThisError>() {
130//!             // We forgot to explicitly convert our error,
131//!             // so this will never run
132//!             unreachable!("This will never run");
133//!         }
134//!
135//!         // So, to be safe, we can start attempting to downcast
136//!         // all the error types that `ThisError` supports?
137//!         if let Some(x) = e.downcast_ref::<sqlx::Error>() {
138//!             // That's okay if ThisError is relatively small,
139//!             // but it's error prone in that we have to remember
140//!             // to add another downcast attempt for any new
141//!             // error variants that are added to `ThisError`
142//!         }
143//!     }
144//!
145//!     Ok(())
146//! }
147//! ```
148//!
149//! # Solution
150//!
151//! This crate bridges the two worlds, allowing you to add context to your [thiserror] type
152//! while preserving the ergonomics and accessibility of the underlying error enum.
153//!
154//! This crate is intended to be used with [thiserror] enums, but should work with any error type.
155//!
156//! ** Example **
157//! ```
158//! use thiserror::Error;
159//! use thiserror_context::{Context, impl_context};
160//!
161//! // A normal, run-of-the-mill thiserror enum
162//! #[derive(Debug, Error)]
163//! enum ThisErrorInner {
164//!     #[error("placeholder err")]
165//!     Placeholder,
166//!
167//!     #[error("sqlx err: {0}")]
168//!     Sqlx(#[from] sqlx::Error),
169//! }
170//!
171//! // Defines a new type, `ThisErr`, that wraps `ThisErrorInner` and allows
172//! // additional context to be added.
173//! impl_context!(ThisError(ThisErrorInner));
174//!
175//! // We are returning the wrapped new type, `ThisError`, instead of the
176//! // underlying `ThisErrorInner`.
177//! async fn my_fn() -> Result<(), ThisError> {
178//!     async {
179//!         // Some db query or something
180//!        Err(sqlx::Error::RowNotFound)
181//!     }.await
182//!         .context("my_fn")?;
183//!     Ok(())
184//! }
185//!
186//! async fn caller() -> anyhow::Result<()> {
187//!     let r: Result<(), ThisError> = my_fn().await;
188//!
189//!     if let Err(e) = r {
190//!         // We can now match on the error type!
191//!         match e.as_ref() {
192//!             ThisErrorInner::Placeholder => {},
193//!             ThisErrorInner::Sqlx(_) => {},
194//!         }
195//!     }
196//!
197//!     Ok(())
198//! }
199//! ```
200//!
201//! # Usage
202//!
203//! Similar to [context], this crate provides a [Context] trait that extends
204//! the [Result] type with two methods: `context` and `with_context`.
205//!
206//! [context](Context::context) adds static context to the error, while
207//! [with_context](Context::with_context) adds dynamic context to the error.
208//!
209//! ```
210//! use thiserror::Error;
211//! use thiserror_context::{Context, impl_context};
212//!
213//! #[derive(Debug, Error)]
214//! enum ThisErrorInner {
215//!     #[error("placeholder err")]
216//!     Placeholder,
217//! }
218//! impl_context!(ThisError(ThisErrorInner));
219//!
220//! fn f(id: i64) -> Result<(), ThisError> {
221//!     Err(ThisErrorInner::Placeholder.into())
222//! }
223//!
224//! fn t(id: i64) -> Result<(), ThisError> {
225//!     f(id)
226//!         .context("some static context")
227//!         .with_context(|| format!("for id {}", id))
228//! }
229//!
230//! let res = t(1);
231//! assert!(res.is_err());
232//! let err = res.unwrap_err();
233//! let debug_repr = format!("{:#?}", err);
234//! assert_eq!(r#"Placeholder
235//!
236//! Caused by:
237//!     0: for id 1
238//!     1: some static context
239//! "#, debug_repr);
240//! ```
241//!
242//! # Nesting
243//!
244//! Context enriched errors can be nested and they will preserve their
245//! context messages when converting from a child error type to a parent.
246//!
247//! See [impl_from_carry_context] for more information.
248pub mod composition;
249
250use std::fmt::Display;
251
252/// Defines a new struct that wraps the error type, allowing additional
253/// context to be added.
254///
255/// The wrapped error type is intended to be a [thiserror](https://docs.rs/thiserror) enum, but
256/// should work with any error type.
257///
258/// The wrapper will implement all the [From] methods that the wrapped
259/// error type implements.
260///
261/// Ultimately, this allows [anyhow](https://docs.rs/anyhow)-like `context` and `with_context`
262/// calls on a strongly typed error.
263///
264/// ** Example **
265/// ```ignore
266/// impl_context!(DummyError(DummyErrorInner));
267/// ```
268#[macro_export]
269macro_rules! impl_context {
270    ($out:ident($ty:ty)) => {
271        impl<T: Into<$ty>> From<T> for $out {
272            fn from(value: T) -> Self {
273                $out::Base(value.into())
274            }
275        }
276
277        pub enum $out {
278            Base($ty),
279            Context { error: Box<$out>, context: String },
280        }
281
282        impl $out {
283            pub fn into_inner(self) -> $ty {
284                match self {
285                    $out::Base(b) => b,
286                    $out::Context { error, .. } => error.into_inner(),
287                }
288            }
289
290            /// Returns all the context messages and the root error.
291            fn all<'a>(&'a self, mut context: Vec<&'a String>) -> (Vec<&'a String>, &'a $ty) {
292                match self {
293                    $out::Base(b) => (context, b),
294                    $out::Context { error, context: c } => {
295                        context.push(c);
296                        error.all(context)
297                    }
298                }
299            }
300        }
301
302        impl std::fmt::Debug for $out {
303            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
304                let (context, root) = self.all(vec![]);
305                f.write_fmt(format_args!("{:?}", root))?; //Caused by:\n", root))?;
306
307                if !context.is_empty() {
308                    f.write_fmt(format_args!("\n\nCaused by:\n"))?;
309                }
310
311                for (i, context) in context.iter().enumerate() {
312                    f.write_fmt(format_args!("    {i}: {}\n", context))?;
313                }
314                Ok(())
315            }
316        }
317
318        impl std::fmt::Display for $out {
319            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320                f.write_fmt(format_args!("{}", self.as_ref()))
321            }
322        }
323
324        impl std::error::Error for $out {}
325
326        impl AsRef<$ty> for $out {
327            fn as_ref(&self) -> &$ty {
328                match self {
329                    $out::Base(b) => b,
330                    // One to get out of the Box, and another to
331                    // recursively call this method.
332                    $out::Context { error, .. } => error.as_ref().as_ref(),
333                }
334            }
335        }
336
337        impl<Z, E: Into<$out>> Context<$out, Z, E> for Result<Z, E> {
338            fn context<C>(self, context: C) -> Result<Z, $out>
339            where
340                C: std::fmt::Display + Send + Sync + 'static,
341            {
342                match self {
343                    Ok(t) => Ok(t),
344                    Err(e) => {
345                        let out: $out = e.into();
346                        Err($out::Context {
347                            error: Box::new(out),
348                            context: context.to_string(),
349                        })
350                    }
351                }
352            }
353
354            fn with_context<C, F>(self, f: F) -> Result<Z, $out>
355            where
356                C: std::fmt::Display + Send + Sync + 'static,
357                F: FnOnce() -> C,
358            {
359                match self {
360                    Ok(t) => Ok(t),
361                    Err(e) => {
362                        let out: $out = e.into();
363                        Err($out::Context {
364                            error: Box::new(out),
365                            context: format!("{}", f()),
366                        })
367                    }
368                }
369            }
370        }
371    };
372}
373
374pub trait Context<W, T, E>
375where
376    E: Into<W>,
377{
378    /// Wrap the error value with additional context.
379    fn context<C>(self, context: C) -> Result<T, W>
380    where
381        C: Display + Send + Sync + 'static;
382
383    /// Wrap the error value with additional context that is evaluated lazily
384    /// only once an error does occur.
385    fn with_context<C, F>(self, f: F) -> Result<T, W>
386    where
387        C: Display + Send + Sync + 'static,
388        F: FnOnce() -> C;
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use thiserror::Error;
395
396    #[derive(Debug, Error)]
397    pub enum DummyErrorInner {
398        #[error("dummy err msg")]
399        Dummy,
400        #[error("parse int err: {0}")]
401        ParseInt(#[from] std::num::ParseIntError),
402    }
403
404    impl_context!(DummyError(DummyErrorInner));
405
406    fn t() -> Result<(), DummyError> {
407        let _: i64 = "fake".parse()?;
408        Ok(())
409    }
410
411    #[test]
412    fn it_works() {
413        let r = t()
414            .context("first")
415            .with_context::<String, _>(|| format!("second dynamic"))
416            .context("third"); //: Result<(), DummyError> = Err(DummyErrorInner::Dummy.into());
417
418        let res = format!("{:#?}", r.as_ref().unwrap_err());
419
420        assert_eq!(
421            res,
422            "ParseInt(ParseIntError { kind: InvalidDigit })\n\nCaused by:\n    0: third\n    1: second dynamic\n    2: first\n"
423        );
424    }
425
426    #[test]
427    fn it_works2() {
428        let r = t().context("parsing test").context("second");
429
430        let r = format!("{:#?}", r.unwrap_err());
431
432        assert_eq!(
433            r,
434            "ParseInt(ParseIntError { kind: InvalidDigit })\n\nCaused by:\n    0: second\n    1: parsing test\n",
435        );
436    }
437
438    #[test]
439    fn no_contrext_omits_causation() {
440        let r = t();
441
442        let r = format!("{:#?}", r.unwrap_err());
443
444        assert_eq!(r, "ParseInt(ParseIntError { kind: InvalidDigit })",);
445    }
446
447    #[test]
448    fn multiple_errors_same_from() {
449        use crate::Context;
450
451        #[derive(Debug, Error)]
452        pub enum AnotherDummyErrorInner {
453            #[error("dummy err msg")]
454            Dummy,
455            #[error("parse int err: {0}")]
456            ParseInt(#[from] std::num::ParseIntError),
457        }
458        impl_context!(AnotherDummyError(AnotherDummyErrorInner));
459        fn v() -> Result<(), AnotherDummyError> {
460            let _: i64 = "fake".parse()?;
461            Ok(())
462        }
463        assert!(v()
464            .context("Adding context shouldn't cause build error")
465            .is_err());
466    }
467}
468
469#[cfg(test)]
470mod composable_tests {
471    use super::*;
472    use inner::DummyError;
473    use thiserror::Error;
474
475    mod inner {
476        use crate::Context;
477        use thiserror::Error;
478
479        #[derive(Debug, Error)]
480        pub enum DummyErrorInner {
481            #[error("dummy err msg")]
482            Dummy,
483            #[error("parse int err: {0}")]
484            ParseInt(#[from] std::num::ParseIntError),
485        }
486        impl_context!(DummyError(DummyErrorInner));
487
488        pub fn t() -> Result<(), DummyError> {
489            Err::<(), DummyErrorInner>(DummyErrorInner::Dummy.into())
490                .context("first")
491                .context("second")
492        }
493    }
494
495    #[derive(Debug, Error)]
496    pub enum OtherInner {
497        #[error("t {0}")]
498        T(DummyError),
499    }
500
501    impl_from_carry_context!(DummyError, Other, OtherInner::T);
502
503    impl_context!(Other(OtherInner));
504
505    fn wrapped() -> Result<(), Other> {
506        _wrapped().context("fourth")
507    }
508
509    fn _wrapped() -> Result<(), Other> {
510        let r: Result<(), DummyError> = inner::t();
511        r.context("third")
512    }
513
514    #[test]
515    fn it_is_composable() {
516        let r = wrapped();
517
518        let r = format!("{:#?}", r.unwrap_err());
519
520        assert_eq!(
521            r,
522            "T(Dummy)\n\nCaused by:\n    0: fourth\n    1: third\n    2: second\n    3: first\n",
523        );
524    }
525}