error_stack/hook/
mod.rs

1pub(crate) mod context;
2
3#[cfg_attr(feature = "std", allow(unused_imports))]
4use alloc::vec::Vec;
5
6use crate::{
7    fmt::{install_builtin_hooks, Hooks},
8    Report,
9};
10
11#[cfg(feature = "std")]
12type RwLock<T> = std::sync::RwLock<T>;
13
14// Generally the std mutex is faster than spin, so if both `std` and `hooks` is enabled we use the
15// std variant.
16#[cfg(all(not(feature = "std"), feature = "hooks"))]
17type RwLock<T> = spin::rwlock::RwLock<T>;
18
19static FMT_HOOK: RwLock<Hooks> = RwLock::new(Hooks { inner: Vec::new() });
20
21impl Report<()> {
22    /// Can be used to globally set a [`Debug`] format hook, for a specific type `T`.
23    ///
24    /// This hook will be called on every [`Debug`] call, if an attachment with the same type has
25    /// been found.
26    ///
27    /// [`Debug`]: core::fmt::Debug
28    ///
29    /// # Examples
30    ///
31    /// ```rust
32    /// # // we only test the snapshot on nightly, therefore report is unused (so is render)
33    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
34    /// use std::io::{Error, ErrorKind};
35    ///
36    /// use error_stack::{
37    ///     report, Report,
38    /// };
39    ///
40    /// struct Suggestion(&'static str);
41    ///
42    /// Report::install_debug_hook::<Suggestion>(|value, context| {
43    ///     context.push_body(format!("suggestion: {}", value.0));
44    /// });
45    ///
46    /// let report =
47    ///     report!(Error::from(ErrorKind::InvalidInput)).attach(Suggestion("oh no, try again"));
48    ///
49    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
50    /// # fn render(value: String) -> String {
51    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
52    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
53    /// #
54    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
55    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
56    /// #
57    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
58    /// # }
59    /// #
60    /// # #[cfg(nightly)]
61    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook.snap")].assert_eq(&render(format!("{report:?}")));
62    /// #
63    /// println!("{report:?}");
64    /// ```
65    ///
66    /// Which will result in something like:
67    ///
68    /// <pre>
69    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook.snap"))]
70    /// </pre>
71    ///
72    /// This example showcases the ability of hooks to be invoked for values provided via the
73    /// Provider API using [`Error::provide`].
74    ///
75    /// ```rust
76    /// # // this is a lot of boilerplate, if you find a better way, please change this!
77    /// # // with #![cfg(nightly)] docsrs will complain that there's no main in non-nightly
78    /// # #![cfg_attr(nightly, feature(error_generic_member_access))]
79    /// # const _: &'static str = r#"
80    /// #![feature(error_generic_member_access)]
81    /// # "#;
82    ///
83    /// # #[cfg(nightly)]
84    /// # mod nightly {
85    /// use core::error::{Request, Error};
86    /// use core::fmt::{Display, Formatter};
87    /// use error_stack::{Report, report};
88    ///
89    /// struct Suggestion(&'static str);
90    ///
91    /// #[derive(Debug)]
92    /// struct ErrorCode(u64);
93    ///
94    ///
95    /// #[derive(Debug)]
96    /// struct UserError {
97    ///     code: ErrorCode
98    /// }
99    ///
100    /// impl Display for UserError {
101    ///     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102    ///         f.write_str("invalid user input")
103    ///     }
104    /// }
105    ///
106    /// impl Error for UserError {
107    ///  fn provide<'a>(&'a self, req: &mut Request<'a>) {
108    ///    req.provide_value(Suggestion("try better next time!"));
109    ///    req.provide_ref(&self.code);
110    ///  }
111    /// }
112    ///
113    /// # pub fn main() {
114    /// Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
115    ///     context.push_body(format!("suggestion: {value}"));
116    /// });
117    /// Report::install_debug_hook::<ErrorCode>(|ErrorCode(value), context| {
118    ///     context.push_body(format!("error code: {value}"));
119    /// });
120    ///
121    /// let report = report!(UserError {code: ErrorCode(420)});
122    ///
123    /// # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
124    /// # fn render(value: String) -> String {
125    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
126    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
127    /// #
128    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
129    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
130    /// #
131    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
132    /// # }
133    /// #
134    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook_provide.snap")].assert_eq(&render(format!("{report:?}")));
135    /// #
136    /// println!("{report:?}");
137    /// # }
138    /// # }
139    /// # #[cfg(not(nightly))]
140    /// # fn main() {}
141    /// # #[cfg(nightly)]
142    /// # fn main() {nightly::main()}
143    /// ```
144    ///
145    /// Which will result in something like:
146    ///
147    /// <pre>
148    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__debug_hook_provide.snap"))]
149    /// </pre>
150    ///
151    /// `error-stack` comes with some built-in hooks which can be overwritten. This is useful if you
152    /// want to change the output of the built-in hooks, or if you want to add additional
153    /// information to the output. For example, you can override the built-in hook for [`Location`]
154    /// to hide the file path:
155    ///
156    /// ```rust
157    /// # // we only test the snapshot on nightly, therefore report is unused (so is render)
158    /// # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
159    /// use std::{
160    ///     io::{Error, ErrorKind},
161    ///     panic::Location,
162    /// };
163    ///
164    /// error_stack::Report::install_debug_hook::<Location>(|_location, _context| {
165    ///     // Intentionally left empty so nothing will be printed
166    /// });
167    ///
168    /// let report = error_stack::report!(Error::from(ErrorKind::InvalidInput));
169    ///
170    /// # error_stack::Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
171    /// # fn render(value: String) -> String {
172    /// #     let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?:  .*\n)*  .*").unwrap();
173    /// #     let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
174    /// #
175    /// #     let value = backtrace.replace_all(&value, "backtrace no. $1\n  [redacted]");
176    /// #     let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
177    /// #
178    /// #     ansi_to_html::convert(value.as_ref()).unwrap()
179    /// # }
180    /// #
181    /// # #[cfg(nightly)]
182    /// # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__location_hook.snap")].assert_eq(&render(format!("{report:?}")));
183    /// #
184    /// println!("{report:?}");
185    /// ```
186    ///
187    /// Which will result in something like:
188    ///
189    /// <pre>
190    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/hook__location_hook.snap"))]
191    /// </pre>
192    ///
193    /// [`Location`]: std::panic::Location
194    /// [`Error::provide`]: std::error::Error::provide
195    #[cfg(any(feature = "std", feature = "hooks"))]
196    pub fn install_debug_hook<T: Send + Sync + 'static>(
197        hook: impl Fn(&T, &mut crate::fmt::HookContext<T>) + Send + Sync + 'static,
198    ) {
199        install_builtin_hooks();
200
201        // TODO: Use `let ... else` when MSRV is 1.65
202        #[cfg(feature = "std")]
203        let mut lock = FMT_HOOK.write().unwrap_or_else(|_| {
204            unreachable!(
205                "Hook is poisoned. This is considered a bug and should be reported to \
206                https://github.com/hashintel/hash/issues/new/choose"
207            )
208        });
209
210        // The spin RwLock cannot panic
211        #[cfg(all(not(feature = "std"), feature = "hooks"))]
212        let mut lock = FMT_HOOK.write();
213
214        lock.insert(hook);
215    }
216
217    /// Returns the hook that was previously set by [`install_debug_hook`]
218    ///
219    /// [`install_debug_hook`]: Self::install_debug_hook
220    #[cfg(any(feature = "std", feature = "hooks"))]
221    pub(crate) fn invoke_debug_format_hook<T>(closure: impl FnOnce(&Hooks) -> T) -> T {
222        install_builtin_hooks();
223
224        // TODO: Use `let ... else` when MSRV is 1.65
225        #[cfg(feature = "std")]
226        let hook = FMT_HOOK.read().unwrap_or_else(|_| {
227            unreachable!(
228                "Hook is poisoned. This is considered a bug and should be reported to \
229                https://github.com/hashintel/hash/issues/new/choose"
230            )
231        });
232
233        // The spin RwLock cannot panic
234        #[cfg(all(not(feature = "std"), feature = "hooks"))]
235        let hook = FMT_HOOK.read();
236
237        closure(&hook)
238    }
239}