human_errors/
error.rs

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