human_errors/
error.rs

1use std::{error, fmt};
2
3/// The kind of error which occurred.
4///
5/// Distinguishes between errors which were the result of user actions
6/// and those which were the result of system failures. Conceptually
7/// similar to HTTP status codes in that 4xx errors are user-caused
8/// and 5xx errors are system-caused.
9#[derive(Debug, PartialEq, Eq)]
10pub enum Kind {
11    /// An error which was the result of actions that the user took.
12    ///
13    /// These errors are usually things which a user can easily resolve by
14    /// changing how they interact with the system. Advice should be used
15    /// to guide the user to the correct interaction paths and help them
16    /// self-mitigate without needing to open support tickets.
17    ///
18    /// These errors are usually generated with [`crate::user`], [`crate::user_with_cause`]
19    /// and [`crate::user_with_internal`].
20    User,
21
22    /// An error which was the result of the system failing rather than the user's actions.
23    ///
24    /// These kinds of issues are usually the result of the system entering
25    /// an unexpected state and/or violating an assumption on behalf of the
26    /// developer. Often these issues cannot be resolved by the user directly,
27    /// so the advice should guide them to the best way to raise a bug with you
28    /// and provide you with information to help them fix the issue.
29    ///
30    /// These errors are usually generated with [`crate::system`], [`crate::system_with_cause`]
31    /// and [`crate::system_with_internal`].
32    System,
33}
34
35impl Kind {
36    fn format_description(&self, description: &str) -> String {
37        match self {
38            Kind::User => format!("Oh no! {description}"),
39            Kind::System => format!("Whoops! {description} (This isn't your fault)"),
40        }
41    }
42}
43
44/// The fundamental error type used by this library.
45///
46/// An error type which encapsulates information about whether an error
47/// is the result of something the user did, or a system failure outside
48/// their control. These errors include a description of what occurred,
49/// advice on how to proceed and references to the causal chain which led
50/// to this failure.
51///
52/// # Examples
53/// ```
54/// use human_errors;
55///
56/// let err = human_errors::user(
57///   "We could not open the config file you provided.",
58///   &["Make sure that the file exists and is readable by the application."],
59/// );
60///
61/// // Prints the error and any advice for the user.
62/// println!("{}", err)
63/// ```
64#[derive(Debug)]
65pub struct Error {
66    kind: Kind,
67    error: Box<dyn error::Error + Send + Sync>,
68    advice: &'static [&'static str],
69}
70
71impl Error {
72    /// Constructs a new [Error].
73    ///
74    /// # Examples
75    /// ```
76    /// use human_errors;
77    /// let err = human_errors::Error::new(
78    ///     "Low-level IO error details",
79    ///     human_errors::Kind::System,
80    ///     &["Try restarting the application", "If the problem persists, contact support"]
81    /// );
82    /// ```
83    pub fn new<E: Into<Box<dyn error::Error + Send + Sync>>>(
84        error: E,
85        kind: Kind,
86        advice: &'static [&'static str],
87    ) -> Self {
88        Self {
89            error: error.into(),
90            kind,
91            advice,
92        }
93    }
94
95    /// Gets the description message from this error.
96    ///
97    /// Gets the description which was provided as the first argument when constructing
98    /// this error.
99    ///
100    /// # Examples
101    /// ```
102    /// use human_errors;
103    ///
104    /// let err = human_errors::user(
105    ///   "We could not open the config file you provided.",
106    ///   &["Make sure that the file exists and is readable by the application."],
107    /// );
108    ///
109    /// // Prints: "We could not open the config file you provided."
110    /// println!("{}", err.description())
111    /// ```
112    pub fn description(&self) -> String {
113        match self.error.downcast_ref::<Error>() {
114            Some(err) => err.description(),
115            None => format!("{}", self.error),
116        }
117    }
118
119    /// Gets the formatted error and its advice.
120    ///
121    /// Generates a string containing the description of the error and any causes,
122    /// as well as a list of suggestions for how a user should
123    /// deal with this error. The "deepest" error's advice is presented first, with
124    /// successively higher errors appearing lower in the list. This is done because
125    /// the most specific error is the one most likely to have the best advice on how
126    /// to resolve the problem.
127    ///
128    /// # Examples
129    /// ```
130    /// use human_errors;
131    ///
132    /// let err = human_errors::wrap_user(
133    ///   human_errors::user(
134    ///     "We could not find a file at /home/user/.config/demo.yml",
135    ///     &["Make sure that the file exists and is readable by the application."]
136    ///   ),
137    ///   "We could not open the config file you provided.",
138    ///   &["Make sure that you've specified a valid config file with the --config option."],
139    /// );
140    ///
141    /// // Prints a message like the following:
142    /// // Oh no! We could not open the config file you provided.
143    /// //
144    /// // This was caused by:
145    /// // We could not find a file at /home/user/.config/demo.yml
146    /// //
147    /// // To try and fix this, you can:
148    /// //  - Make sure that the file exists and is readable by the application.
149    /// //  - Make sure that you've specified a valid config file with the --config option.
150    /// println!("{}", err.message());
151    /// ```
152    pub fn message(&self) -> String {
153        let description = self.description();
154        let hero_message = self.kind.format_description(&description);
155
156        match (self.caused_by(), self.advice()) {
157            (cause, advice) if !cause.is_empty() && !advice.is_empty() => {
158                format!(
159                    "{}\n\nThis was caused by:\n - {}\n\nTo try and fix this, you can:\n - {}",
160                    hero_message,
161                    cause.join("\n - "),
162                    advice.join("\n - ")
163                )
164            }
165            (cause, _) if !cause.is_empty() => {
166                format!(
167                    "{}\n\nThis was caused by:\n - {}",
168                    hero_message,
169                    cause.join("\n - ")
170                )
171            }
172            (_, advice) if !advice.is_empty() => {
173                format!(
174                    "{}\n\nTo try and fix this, you can:\n - {}",
175                    hero_message,
176                    advice.join("\n - ")
177                )
178            }
179            _ => hero_message,
180        }
181    }
182
183    fn caused_by(&self) -> Vec<String> {
184        let mut causes = Vec::new();
185        let mut current_error: &dyn error::Error = self.error.as_ref();
186        while let Some(err) = current_error.source() {
187            if let Some(err) = err.downcast_ref::<Error>() {
188                causes.push(err.description());
189            } else {
190                causes.push(format!("{}", err));
191            }
192
193            current_error = err;
194        }
195
196        causes
197    }
198
199    fn advice(&self) -> Vec<&'static str> {
200        let mut advice = self.advice.to_vec();
201
202        let mut cause = self.error.as_ref();
203        while let Some(err) = cause.downcast_ref::<Error>() {
204            advice.extend_from_slice(err.advice);
205            cause = err.error.as_ref();
206        }
207
208        advice.reverse();
209
210        advice
211    }
212
213    /// Checks if this error is of a specific kind.
214    ///
215    /// Returns `true` if this error matches the provided [Kind],
216    /// otherwise `false`.
217    ///
218    /// # Examples
219    /// ```
220    /// use human_errors;
221    ///
222    /// let err = human_errors::user(
223    ///   "We could not open the config file you provided.",
224    ///   &["Make sure that the file exists and is readable by the application."],
225    /// );
226    ///
227    /// // Prints "is_user?: true"
228    /// println!("is_user?: {}", err.is(human_errors::Kind::User));
229    /// ```
230    pub fn is(&self, kind: Kind) -> bool {
231        self.kind == kind
232    }
233}
234
235impl error::Error for Error {
236    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
237        self.error.source()
238    }
239}
240
241impl fmt::Display for Error {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        write!(f, "{}", self.message())
244    }
245}