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}