Skip to main content

human_errors/
error.rs

1use super::Kind;
2use std::{error, fmt};
3
4#[cfg(feature = "serde")]
5use serde::ser::SerializeStruct;
6
7/// The fundamental error type used by this library.
8///
9/// An error type which encapsulates information about whether an error
10/// is the result of something the user did, or a system failure outside
11/// their control. These errors include a description of what occurred,
12/// advice on how to proceed and references to the causal chain which led
13/// to this failure.
14///
15/// # Examples
16/// ```
17/// use human_errors;
18///
19/// let err = human_errors::user(
20///   "We could not open the config file you provided.",
21///   &["Make sure that the file exists and is readable by the application."],
22/// );
23///
24/// // Prints the error and any advice for the user.
25/// println!("{}", err)
26/// ```
27#[derive(Debug)]
28pub struct Error {
29    pub(crate) kind: Kind,
30    pub(crate) error: Box<dyn error::Error + Send + Sync>,
31    pub(crate) advice: &'static [&'static str],
32    pub(crate) backtrace: Option<std::backtrace::Backtrace>,
33}
34
35impl Error {
36    /// Constructs a new [Error].
37    ///
38    /// # Examples
39    /// ```
40    /// use human_errors;
41    /// let err = human_errors::Error::new(
42    ///     "Low-level IO error details",
43    ///     human_errors::Kind::System,
44    ///     &["Try restarting the application", "If the problem persists, contact support"]
45    /// );
46    /// ```
47    pub fn new<E: Into<Box<dyn error::Error + Send + Sync>>>(
48        error: E,
49        kind: Kind,
50        advice: &'static [&'static str],
51    ) -> Self {
52        Self {
53            error: error.into(),
54            kind,
55            advice,
56            #[cfg(feature = "force_backtraces")]
57            backtrace: Some(std::backtrace::Backtrace::force_capture()),
58            #[cfg(all(not(feature = "force_backtraces"), feature = "backtraces"))]
59            backtrace: Some(std::backtrace::Backtrace::capture()),
60            #[cfg(not(feature = "backtraces"))]
61            backtrace: None,
62        }
63    }
64
65    /// Checks if this error is of a specific kind.
66    ///
67    /// Returns `true` if this error matches the provided [Kind],
68    /// otherwise `false`.
69    ///
70    /// # Examples
71    /// ```
72    /// use human_errors;
73    ///
74    /// let err = human_errors::user(
75    ///   "We could not open the config file you provided.",
76    ///   &["Make sure that the file exists and is readable by the application."],
77    /// );
78    ///
79    /// // Prints "is_user?: true"
80    /// println!("is_user?: {}", err.is(human_errors::Kind::User));
81    /// ```
82    pub fn is(&self, kind: Kind) -> bool {
83        self.kind == kind
84    }
85
86    /// Gets the description message from this error.
87    ///
88    /// Gets the description which was provided as the first argument when constructing
89    /// this error.
90    ///
91    /// # Examples
92    /// ```
93    /// use human_errors;
94    ///
95    /// let err = human_errors::user(
96    ///   "We could not open the config file you provided.",
97    ///   &["Make sure that the file exists and is readable by the application."],
98    /// );
99    ///
100    /// // Prints: "We could not open the config file you provided."
101    /// println!("{}", err.description())
102    /// ```
103    pub fn description(&self) -> String {
104        match self.error.downcast_ref::<Error>() {
105            Some(err) => err.description(),
106            None => format!("{}", self.error),
107        }
108    }
109
110    /// Gets the advice associated with this error and its causes.
111    ///
112    /// Gathers all advice from this error and any causal errors it wraps,
113    /// returning a deduplicated list of suggestions for how a user should
114    /// deal with this error.
115    ///
116    /// # Examples
117    /// ```
118    /// use human_errors;
119    ///
120    /// let err = human_errors::wrap_user(
121    ///   human_errors::user(
122    ///     "We could not find a file at /home/user/.config/demo.yml",
123    ///     &["Make sure that the file exists and is readable by the application."]
124    ///   ),
125    ///   "We could not open the config file you provided.",
126    ///   &["Make sure that you've specified a valid config file with the --config option."],
127    /// );
128    ///
129    /// // Prints:
130    /// // - Make sure that the file exists and is readable by the application.
131    /// // - Make sure that you've specified a valid config file with the --config option.
132    /// for tip in err.advice() {
133    ///     println!("- {}", tip);
134    /// }
135    /// ``````
136    pub fn advice(&self) -> Vec<&'static str> {
137        let mut advice = self.advice.to_vec();
138
139        let mut cause: Option<&(dyn std::error::Error + 'static)> = Some(self.error.as_ref());
140        while let Some(err) = cause {
141            if let Some(err) = err.downcast_ref::<Error>() {
142                advice.extend_from_slice(err.advice);
143            }
144
145            cause = err.source();
146        }
147
148        advice.reverse();
149
150        let mut seen = std::collections::HashSet::new();
151        advice.retain(|item| seen.insert(*item));
152
153        advice
154    }
155
156    /// Gets the formatted error and its advice.
157    ///
158    /// Generates a string containing the description of the error and any causes,
159    /// as well as a list of suggestions for how a user should
160    /// deal with this error. The "deepest" error's advice is presented first, with
161    /// successively higher errors appearing lower in the list. This is done because
162    /// the most specific error is the one most likely to have the best advice on how
163    /// to resolve the problem.
164    ///
165    /// # Examples
166    /// ```
167    /// use human_errors;
168    ///
169    /// let err = human_errors::wrap_user(
170    ///   human_errors::user(
171    ///     "We could not find a file at /home/user/.config/demo.yml",
172    ///     &["Make sure that the file exists and is readable by the application."]
173    ///   ),
174    ///   "We could not open the config file you provided.",
175    ///   &["Make sure that you've specified a valid config file with the --config option."],
176    /// );
177    ///
178    /// // Prints a message like the following:
179    /// // We could not open the config file you provided. (User error)
180    /// //
181    /// // This was caused by:
182    /// // We could not find a file at /home/user/.config/demo.yml
183    /// //
184    /// // To try and fix this, you can:
185    /// //  - Make sure that the file exists and is readable by the application.
186    /// //  - Make sure that you've specified a valid config file with the --config option.
187    /// println!("{}", err.message());
188    /// ```
189    pub fn message(&self) -> String {
190        let description = self.description();
191        let hero_message = self.kind.format_description(&description);
192
193        match (self.caused_by(), self.advice()) {
194            (cause, advice) if !cause.is_empty() && !advice.is_empty() => {
195                format!(
196                    "{}\n\nThis was caused by:\n - {}\n\nTo try and fix this, you can:\n - {}",
197                    hero_message,
198                    cause.join("\n - "),
199                    advice.join("\n - "),
200                )
201            }
202            (cause, _) if !cause.is_empty() => {
203                format!(
204                    "{}\n\nThis was caused by:\n - {}",
205                    hero_message,
206                    cause.join("\n - ")
207                )
208            }
209            (_, advice) if !advice.is_empty() => {
210                format!(
211                    "{}\n\nTo try and fix this, you can:\n - {}",
212                    hero_message,
213                    advice.join("\n - ")
214                )
215            }
216            _ => hero_message,
217        }
218    }
219
220    /// Gets the formatted error, its advice, and any captured backtraces.
221    ///
222    /// Behaves like [`Error::message`], but additionally appends the backtrace
223    /// captured for this error as well as the backtrace of each causal error
224    /// which recorded one. This is primarily useful when diagnosing system
225    /// failures, where the backtraces can help pinpoint where the problem
226    /// originated.
227    ///
228    /// Backtraces are only included when the `backtraces` (or `force_backtraces`)
229    /// feature is enabled and a backtrace was successfully captured (which
230    /// usually requires the `RUST_BACKTRACE` environment variable to be set).
231    /// When no backtraces are available, this method returns the same output as
232    /// [`Error::message`].
233    ///
234    /// # Examples
235    /// ```
236    /// use human_errors;
237    ///
238    /// let err = human_errors::system(
239    ///   "We could not connect to the database.",
240    ///   &["Check that the database is running and reachable."],
241    /// );
242    ///
243    /// // Prints the human-readable message followed by any captured backtraces.
244    /// println!("{}", err.message_with_backtrace());
245    /// ```
246    pub fn message_with_backtrace(&self) -> String {
247        #[cfg(not(feature = "backtraces"))]
248        {
249            self.message()
250        }
251
252        #[cfg(feature = "backtraces")]
253        {
254            let mut message = self.message();
255
256            for (description, backtrace) in crate::backtraces::collect(self) {
257                message.push_str(&format!("\n\nBacktrace ({description}):\n{backtrace}"));
258            }
259
260            message
261        }
262    }
263
264    /// Gets the backtrace associated with this error, if available.
265    ///
266    /// Returns `Some` if the `backtraces` feature is enabled and a backtrace was captured when this error was created, otherwise returns `None`.
267    ///
268    /// # Examples
269    /// ```
270    /// use human_errors;
271    ///
272    /// let err = human_errors::user(
273    ///   "We could not open the config file you provided.",
274    ///   &["Make sure that the file exists and is readable by the application."],
275    /// );
276    ///
277    /// if let Some(backtrace) = err.backtrace() {
278    ///     println!("Backtrace:\n{}", backtrace);
279    /// } else {
280    ///     println!("Backtrace not available.");
281    /// }
282    pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
283        self.backtrace.as_ref()
284    }
285
286    fn caused_by(&self) -> Vec<String> {
287        let mut causes = Vec::new();
288        let mut current_error: &dyn error::Error = self.error.as_ref();
289        while let Some(err) = current_error.source() {
290            if let Some(err) = err.downcast_ref::<Error>() {
291                causes.push(err.description());
292            } else {
293                causes.push(format!("{}", err));
294            }
295
296            current_error = err;
297        }
298
299        causes
300    }
301}
302
303impl error::Error for Error {
304    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
305        self.error.source()
306    }
307}
308
309impl fmt::Display for Error {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        write!(f, "{}", self.message())
312    }
313}
314
315#[cfg(feature = "serde")]
316impl serde::Serialize for Error {
317    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
318    where
319        S: serde::Serializer,
320    {
321        let mut state = serializer.serialize_struct("Error", 3)?;
322        state.serialize_field("kind", &self.kind)?;
323        state.serialize_field("description", &self.description())?;
324        state.serialize_field("advice", &self.advice())?;
325        state.end()
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_basic_user_error() {
335        let err = Error::new(
336            "Something bad happened.",
337            Kind::User,
338            &["Avoid bad things happening in future"],
339        );
340
341        assert!(err.is(Kind::User));
342        assert_eq!(err.description(), "Something bad happened.");
343        assert_eq!(
344            err.message(),
345            "Something bad happened. (User error)\n\nTo try and fix this, you can:\n - Avoid bad things happening in future"
346        );
347    }
348
349    #[test]
350    fn test_basic_system_error() {
351        let err = Error::new(
352            "Something bad happened.",
353            Kind::System,
354            &["Avoid bad things happening in future"],
355        );
356
357        assert!(err.is(Kind::System));
358        assert_eq!(err.description(), "Something bad happened.");
359        assert_eq!(
360            err.message(),
361            "Something bad happened. (System failure)\n\nTo try and fix this, you can:\n - Avoid bad things happening in future"
362        );
363    }
364
365    #[test]
366    fn test_advice_aggregation() {
367        let low_level_err = Error::new(
368            "Low-level failure.",
369            Kind::System,
370            &["Check low-level systems"],
371        );
372
373        let high_level_err = Error::new(
374            low_level_err,
375            Kind::User,
376            &["Check high-level configuration"],
377        );
378
379        assert_eq!(
380            high_level_err.advice(),
381            vec!["Check low-level systems", "Check high-level configuration"]
382        );
383    }
384
385    #[test]
386    fn test_backtrace_capture() {
387        let err = Error::new(
388            "Something bad happened.",
389            Kind::User,
390            &["Avoid bad things happening in future"],
391        );
392
393        #[cfg(feature = "backtraces")]
394        {
395            assert!(err.backtrace().is_some());
396        }
397
398        #[cfg(not(feature = "backtraces"))]
399        {
400            assert!(err.backtrace().is_none());
401        }
402    }
403
404    #[test]
405    fn test_message_with_backtrace() {
406        let err = crate::wrap_system(
407            crate::system("Inner failure.", &["Check inner systems"]),
408            "Outer failure.",
409            &["Check outer configuration"],
410        );
411
412        let message = err.message_with_backtrace();
413
414        // The human-readable message is always the prefix of the output.
415        assert!(message.starts_with(&err.message()));
416
417        #[cfg(feature = "force_backtraces")]
418        {
419            // Both the outer and inner errors captured a backtrace, so both are
420            // rendered recursively.
421            assert_eq!(message.matches("Backtrace (").count(), 2);
422            assert!(message.contains("Backtrace (Outer failure.):"));
423            assert!(message.contains("Backtrace (Inner failure.):"));
424        }
425
426        #[cfg(not(feature = "backtraces"))]
427        {
428            // With no backtraces available the output matches `message`.
429            assert_eq!(message, err.message());
430        }
431    }
432}