Skip to main content

ntex_error/
lib.rs

1//! Error management.
2#![deny(clippy::pedantic)]
3#![allow(
4    clippy::must_use_candidate,
5    clippy::missing_errors_doc,
6    clippy::missing_panics_doc
7)]
8use std::{error::Error as StdError, fmt};
9
10mod bt;
11mod chain;
12mod error;
13mod ext;
14mod info;
15mod message;
16mod repr;
17pub mod utils;
18
19pub use crate::bt::{Backtrace, BacktraceResolver};
20pub use crate::chain::ErrorChain;
21pub use crate::error::Error;
22pub use crate::info::ErrorInfo;
23pub use crate::message::{ErrorMessage, ErrorMessageChained};
24pub use crate::message::{fmt_diag, fmt_diag_string, fmt_err, fmt_err_string};
25pub use crate::utils::{Success, with_service};
26
27#[doc(hidden)]
28pub use crate::bt::{set_backtrace_start, set_backtrace_start_alt};
29
30/// The type of the result.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum ResultType {
33    Success,
34    ClientError,
35    ServiceError,
36}
37
38impl ResultType {
39    /// Returns a str representation of the result type.
40    pub const fn as_str(&self) -> &'static str {
41        match self {
42            ResultType::Success => "Success",
43            ResultType::ClientError => "ClientError",
44            ResultType::ServiceError => "ServiceError",
45        }
46    }
47}
48
49/// Describes how a domain-specific error maps into a unified result model.
50pub trait ResultKind: fmt::Debug + 'static {
51    /// Returns the classification of the result (e.g. success, client error, service error).
52    fn tp(&self) -> ResultType;
53
54    /// Returns a stable identifier for the specific error classification.
55    ///
56    /// It is used for logging, metrics, and diagnostics.
57    fn signature(&self) -> &'static str;
58}
59
60impl ResultKind for ResultType {
61    fn tp(&self) -> ResultType {
62        *self
63    }
64
65    fn signature(&self) -> &'static str {
66        self.as_str()
67    }
68}
69
70/// Provides diagnostic information for errors.
71///
72/// It enables classification, service attribution, and debugging context.
73pub trait ErrorDiagnostic: StdError + 'static {
74    type Kind: ResultKind;
75
76    /// Returns the classification kind of this error.
77    fn kind(&self) -> Self::Kind;
78
79    /// Returns the name of the responsible service, if applicable.
80    ///
81    /// Used to identify upstream or internal service ownership for diagnostics.
82    fn service(&self) -> Option<&'static str> {
83        None
84    }
85
86    /// Returns a backtrace for debugging purposes, if available.
87    fn backtrace(&self) -> Option<&Backtrace> {
88        None
89    }
90
91    #[track_caller]
92    fn chain(self) -> ErrorChain<Self::Kind>
93    where
94        Self: Sized,
95    {
96        ErrorChain::new(self)
97    }
98}
99
100/// Helper trait for converting a value into a unified error-aware result type.
101pub trait ErrorMapping<T, E, U> {
102    /// Converts the value into a `Result`, wrapping it in a structured error type if needed.
103    fn into_error(self) -> Result<T, Error<U>>;
104}
105
106impl fmt::Display for ResultType {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "{}", self.as_str())
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use std::{error::Error as StdError, io, mem};
115
116    use super::*;
117
118    #[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
119    enum TestKind {
120        #[error("Connect")]
121        Connect,
122        #[error("Disconnect")]
123        Disconnect,
124        #[error("ServiceError")]
125        ServiceError,
126    }
127
128    impl ResultKind for TestKind {
129        fn tp(&self) -> ResultType {
130            match self {
131                TestKind::Connect | TestKind::Disconnect => ResultType::ClientError,
132                TestKind::ServiceError => ResultType::ServiceError,
133            }
134        }
135
136        fn signature(&self) -> &'static str {
137            match self {
138                TestKind::Connect => "Client-Connect",
139                TestKind::Disconnect => "Client-Disconnect",
140                TestKind::ServiceError => "Service-Internal",
141            }
142        }
143    }
144
145    #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
146    enum TestError {
147        #[error("Connect err: {0}")]
148        Connect(&'static str),
149        #[error("Disconnect")]
150        Disconnect,
151        #[error("InternalServiceError")]
152        Service(&'static str),
153    }
154
155    impl ErrorDiagnostic for TestError {
156        type Kind = TestKind;
157
158        fn kind(&self) -> Self::Kind {
159            match self {
160                TestError::Connect(_) => TestKind::Connect,
161                TestError::Disconnect => TestKind::Disconnect,
162                TestError::Service(_) => TestKind::ServiceError,
163            }
164        }
165
166        fn service(&self) -> Option<&'static str> {
167            Some("test")
168        }
169    }
170
171    #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
172    #[error("TestError2")]
173    struct TestError2;
174    impl ErrorDiagnostic for TestError2 {
175        type Kind = ResultType;
176
177        fn kind(&self) -> Self::Kind {
178            ResultType::ClientError
179        }
180    }
181
182    impl From<TestError> for TestError2 {
183        fn from(_err: TestError) -> TestError2 {
184            TestError2
185        }
186    }
187
188    #[ntex::test]
189    async fn test_error() {
190        let err: Error<TestError> = TestError::Service("409 Error").into();
191        let err = err.clone();
192        assert_eq!(err.kind(), TestKind::ServiceError);
193        assert_eq!((*err).kind(), TestKind::ServiceError);
194        assert_eq!(err.to_string(), "InternalServiceError");
195        assert_eq!(err.service(), Some("test"));
196        assert_eq!(err.kind().signature(), "Service-Internal");
197        assert_eq!(
198            err,
199            Into::<Error<TestError>>::into(TestError::Service("409 Error"))
200        );
201        assert!(err.backtrace().is_some());
202
203        let err = err.set_service("SVC");
204        assert_eq!(err.service(), Some("SVC"));
205
206        let err2: Error<TestError> = Error::new(TestError::Service("409 Error"), "TEST");
207        assert!(err != err2);
208        assert_eq!(err, TestError::Service("409 Error"));
209
210        let err2 = err2.set_service("SVC");
211        assert_eq!(err, err2);
212        let err2 = err2.map(|_| TestError::Disconnect);
213        assert!(err != err2);
214        let err2 = err2.forward(|_| TestError::Disconnect);
215        assert!(err != err2);
216
217        assert_eq!(TestError::Connect("").kind().tp(), ResultType::ClientError);
218        assert_eq!(TestError::Disconnect.kind().tp(), ResultType::ClientError);
219        assert_eq!(TestError::Service("").kind().tp(), ResultType::ServiceError);
220        assert_eq!(TestError::Connect("").to_string(), "Connect err: ");
221        assert_eq!(TestError::Disconnect.to_string(), "Disconnect");
222        assert_eq!(TestError::Disconnect.service(), Some("test"));
223        assert!(TestError::Disconnect.backtrace().is_none());
224
225        assert_eq!(ResultType::ClientError.as_str(), "ClientError");
226        assert_eq!(ResultType::ServiceError.as_str(), "ServiceError");
227        assert_eq!(ResultType::ClientError.tp(), ResultType::ClientError);
228        assert_eq!(ResultType::ServiceError.tp(), ResultType::ServiceError);
229        assert_eq!(ResultType::ClientError.to_string(), "ClientError");
230        assert_eq!(ResultType::ServiceError.to_string(), "ServiceError");
231        assert_eq!(format!("{}", ResultType::ClientError), "ClientError");
232
233        assert_eq!(TestKind::Connect.to_string(), "Connect");
234        assert_eq!(TestError::Connect("").kind().signature(), "Client-Connect");
235        assert_eq!(TestKind::Disconnect.to_string(), "Disconnect");
236        assert_eq!(
237            TestError::Disconnect.kind().signature(),
238            "Client-Disconnect"
239        );
240        assert_eq!(TestKind::ServiceError.to_string(), "ServiceError");
241        assert_eq!(
242            TestError::Service("").kind().signature(),
243            "Service-Internal"
244        );
245
246        let err = err.into_error().chain();
247        assert_eq!(err.kind(), TestKind::ServiceError);
248        assert_eq!(err.kind(), TestError::Service("409 Error").kind());
249        assert_eq!(err.to_string(), "InternalServiceError");
250        assert!(format!("{err:?}").contains("Service(\"409 Error\")"));
251        assert!(
252            format!("{:?}", err.source()).contains("Service(\"409 Error\")"),
253            "{:?}",
254            err.source().unwrap()
255        );
256
257        let err: Error<TestError> = TestError::Service("404 Error").into();
258        if let Some(bt) = err.backtrace() {
259            bt.resolver().resolve();
260            assert!(
261                format!("{bt}").contains("ntex_error::tests::test_error"),
262                "{bt}",
263            );
264            assert!(
265                bt.repr().unwrap().contains("ntex_error::tests::test_error"),
266                "{bt}"
267            );
268        }
269
270        let err: ErrorChain<TestKind> = err.into();
271        assert_eq!(err.kind(), TestKind::ServiceError);
272        assert_eq!(err.kind(), TestError::Service("404 Error").kind());
273        assert_eq!(err.service(), Some("test"));
274        assert_eq!(err.kind().signature(), "Service-Internal");
275        assert_eq!(err.to_string(), "InternalServiceError");
276        assert!(err.backtrace().is_some());
277        assert!(format!("{err:?}").contains("Service(\"404 Error\")"));
278
279        assert_eq!(24, mem::size_of::<TestError>());
280        assert_eq!(8, mem::size_of::<Error<TestError>>());
281
282        assert_eq!(TestError2.service(), None);
283        assert_eq!(TestError2.kind().signature(), "ClientError");
284
285        // ErrorInformation
286        let err: Error<TestError> = TestError::Service("409 Error").into();
287        let msg = fmt_err_string(&err);
288        assert_eq!(msg, "InternalServiceError\n");
289        let msg = fmt_diag_string(&err);
290        assert!(msg.contains("err: InternalServiceError"));
291
292        let err: ErrorInfo = err.set_service("SVC").into();
293        assert_eq!(err.tp(), ResultType::ServiceError);
294        assert_eq!(err.service(), Some("SVC"));
295        assert_eq!(err.signature(), "Service-Internal");
296        assert!(err.backtrace().is_some());
297
298        let res = Err(TestError::Service("409 Error"));
299        let res: Result<(), Error<TestError>> = res.into_error();
300        let _res: Result<(), Error<TestError2>> = res.into_error();
301
302        let msg = fmt_err_string(&err);
303        assert_eq!(msg, "InternalServiceError\n");
304        let msg = fmt_diag_string(&err);
305        assert!(msg.contains("err: InternalServiceError"));
306
307        // Error extensions
308        let err: Error<TestError> = TestError::Service("409 Error").into();
309        assert_eq!(err.get_item::<&str>(), None);
310        let err = err.insert_item("Test");
311        assert_eq!(err.get_item::<&str>(), Some(&"Test"));
312        let err2 = err.clone();
313        assert_eq!(err2.get_item::<&str>(), Some(&"Test"));
314        let err2 = err2.insert_item("Test2");
315        assert_eq!(err2.get_item::<&str>(), Some(&"Test2"));
316        assert_eq!(err.get_item::<&str>(), Some(&"Test"));
317        let err2 = err.clone().map(|_| TestError::Disconnect);
318        assert_eq!(err2.get_item::<&str>(), Some(&"Test"));
319
320        let info = ErrorInfo::from(&err2);
321        assert_eq!(info.get_item::<&str>(), Some(&"Test"));
322
323        let err3 = err
324            .clone()
325            .try_map(|_| Err::<(), _>(TestError2))
326            .err()
327            .unwrap();
328        assert_eq!(err3.kind().signature(), "ClientError");
329        assert_eq!(err3.get_item::<&str>(), Some(&"Test"));
330
331        let res = err.clone().try_map(|_| Ok::<_, TestError2>(()));
332        assert_eq!(res, Ok(()));
333
334        assert_eq!(Success.kind(), ResultType::Success);
335        assert_eq!(format!("{Success}"), "Success");
336
337        assert_eq!(
338            ErrorDiagnostic::kind(&io::Error::other("")),
339            ResultType::ServiceError
340        );
341        assert_eq!(
342            ErrorDiagnostic::kind(&io::Error::new(io::ErrorKind::InvalidData, "")),
343            ResultType::ClientError
344        );
345    }
346}