nym_common/
error.rs

1// Copyright 2016-2024 Mullvad VPN AB. All Rights Reserved.
2// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
3// SPDX-License-Identifier: GPL-3.0-only
4
5use std::{error::Error, fmt, fmt::Write};
6use tracing::error;
7
8/// Used to generate string representations of error chains.
9pub trait ErrorExt {
10    /// Creates a string representation of the entire error chain.
11    fn display_chain(&self) -> String;
12
13    /// Like [Self::display_chain] but with an extra message at the start of the chain.
14    fn display_chain_with_msg<S: AsRef<str>>(&self, msg: S) -> String;
15}
16
17impl<E: Error> ErrorExt for E {
18    fn display_chain(&self) -> String {
19        let mut s = format!("Error: {self}");
20        let mut source = self.source();
21        while let Some(error) = source {
22            if let Err(err) = write!(&mut s, "\nCaused by: {error}") {
23                error!("error formatting failure: {err}");
24            }
25            source = error.source();
26        }
27        s
28    }
29
30    fn display_chain_with_msg<S: AsRef<str>>(&self, msg: S) -> String {
31        let mut s = format!("Error: {}\nCaused by: {}", msg.as_ref(), self);
32        let mut source = self.source();
33        while let Some(error) = source {
34            if let Err(err) = write!(&mut s, "\nCaused by: {error}") {
35                error!("error formatting failure: {err}");
36            }
37            source = error.source();
38        }
39        s
40    }
41}
42
43#[macro_export]
44macro_rules! trace_err_chain {
45    ($err:expr) => {
46        tracing::error!("{}", $crate::ErrorExt::display_chain(&$err));
47    };
48    ($err:expr, $($args:tt)*) => {
49        tracing::error!("{}", $crate::ErrorExt::display_chain_with_msg(&$err, ::std::format!($($args)*)));
50    };
51}
52
53#[cfg(test)]
54mod tests {
55    use tracing_test::traced_test;
56
57    use std::{io, path::PathBuf};
58
59    #[test]
60    #[traced_test]
61    fn test_trace_err_chain() {
62        trace_err_chain!(io::Error::other("file not found"));
63        assert!(logs_contain("Error: file not found"));
64    }
65
66    #[test]
67    #[traced_test]
68    fn test_trace_err_chain_with_msg() {
69        trace_err_chain!(io::Error::other("file not found"), "failed to open file");
70        assert!(logs_contain("Error: failed to open file"));
71        // todo: fix once it supports multiline messages
72        // https://github.com/dbrgn/tracing-test/issues/48
73        // assert!(logs_contain("Caused by: file not found"));
74    }
75
76    #[test]
77    #[traced_test]
78    fn test_trace_err_chain_with_msgfmt() {
79        trace_err_chain!(
80            io::Error::other("file not found"),
81            "failed to open file: {}",
82            PathBuf::from("test.txt").display()
83        );
84        assert!(logs_contain("Error: failed to open file: test.txt"));
85        // todo: fix once it supports multiline messages
86        // https://github.com/dbrgn/tracing-test/issues/48
87        // assert!(logs_contain("Caused by: file not found"));
88    }
89}
90#[derive(Debug)]
91pub struct BoxedError(Box<dyn Error + 'static + Send>);
92
93impl fmt::Display for BoxedError {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        self.0.fmt(f)
96    }
97}
98
99impl Error for BoxedError {
100    fn source(&self) -> Option<&(dyn Error + 'static)> {
101        self.0.source()
102    }
103}
104
105impl BoxedError {
106    pub fn new(error: impl Error + 'static + Send) -> Self {
107        BoxedError(Box::new(error))
108    }
109}