error_stack/hook/
mod.rs

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