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}