bherror/
lib.rs

1// Copyright (C) 2020-2025  The Blockhouse Technology Limited (TBTL).
2//
3// This program is free software: you can redistribute it and/or modify it
4// under the terms of the GNU Affero General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or (at your
6// option) any later version.
7//
8// This program is distributed in the hope that it will be useful, but
9// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
11// License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16#![deny(missing_docs)]
17#![deny(rustdoc::broken_intra_doc_links)]
18
19//! This crate provides an error handling system used across all of the TBTL's Rust code.
20//!
21//! The errors constructed are automatically logged as warnings. Errors also carry the backtrace of
22//! source errors with them, along with extra context if any.
23//!
24//! # Details
25//!
26//! Use `std::result::Result<T, bherror::Error<E>>`, or equivalently `bherror::Result<T, E>` as the
27//! return type for functions which may return an error.
28//!
29//! The error type `E` in `bherror::Error<E>` must implement the [`BhError`] trait.  Therefore, all
30//! of our concrete error types must implement [`BhError`].
31//!
32//! Constructing the initial, root error is done via the [`Error::root`] method.  This will also
33//! log a warning.
34//!
35//! Error types that are not defined by us, i.e. don't implement [`BhError`] but do implement
36//! [`std::error::Error`] we name as "foreign errors".  These errors can be converted & propagated
37//! to `bherror::Error<E>` via the [`ForeignError`][traits::ForeignError] trait.
38//!
39//! Propagating `bherror::Error<E>` types is done via the [`PropagateError`][traits::ForeignError]
40//! trait, instead of using `?`.  This way we preserve the trace of source errors.
41//!
42//! Additional context can be attached to an error using the [`Error::ctx`] method.  As a
43//! convenience, we also offer [`ErrorContext`][traits::ErrorContext] trait which extends the
44//! [`Result`] type with the same method.
45//!
46//! The crate also offers some additional features.
47//!
48//! * [`ErrorDyn`] for cases when you want to type-erase the concrete [`BhError`] type.
49//! * [`Loggable`][traits::Loggable] trait which extends the [`Result`] with a method for logging
50//!   errors at the error level.  Note, we log all constructed errors as warnings regardless.
51//! * [`adapters`] module for easier integration with other libraries & frameworks.
52//!
53//! # Examples
54//!
55//! ```
56//! use bherror::traits::{ErrorContext, ForeignError, PropagateError};
57//!
58//! enum MyErrors {
59//!     NumberIsNegativeError,
60//!     NumberParseError,
61//! }
62//!
63//! impl std::fmt::Display for MyErrors {
64//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65//!         match self {
66//!             MyErrors::NumberIsNegativeError => write!(f, "MyErrors::NumberIsNegativeError"),
67//!             MyErrors::NumberParseError => write!(f, "MyErrors::NumberParseError"),
68//!         }
69//!     }
70//! }
71//!
72//! impl bherror::BhError for MyErrors {}
73//!
74//! fn my_function(s: &str) -> bherror::Result<i32, MyErrors> {
75//!     let num = s
76//!         .parse()
77//!         // Propagate a "foreign error" and log it as a warning.
78//!         .foreign_err(|| MyErrors::NumberParseError)
79//!         // Add some additional context to the error.
80//!         .ctx(|| format!("parsing {s}"))?;
81//!     if num < 0 {
82//!         // Return the root error and log it as a warning.
83//!         Err(bherror::Error::root(MyErrors::NumberIsNegativeError))
84//!     } else {
85//!         Ok(num)
86//!     }
87//! }
88//!
89//! struct AnotherError;
90//!
91//! impl std::fmt::Display for AnotherError {
92//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93//!         write!(f, "AnotherError")
94//!     }
95//! }
96//!
97//! impl bherror::BhError for AnotherError {}
98//!
99//! fn another_function() -> bherror::Result<(), AnotherError> {
100//!     // Propagate `MyErrors` as the source error for `AnotherError`
101//!     my_function("blah").with_err(|| AnotherError)?;
102//!     Ok(())
103//! }
104//! ```
105
106use std::{any::Any, ops::Deref};
107
108use crate::traits::loggable::Warnable;
109
110pub mod adapters;
111mod display;
112pub mod traits;
113
114/// The trait needed for compatibility with the [`Error`] functionality.
115pub trait BhError: std::fmt::Display + Send + Sync + 'static {}
116
117/// Hacky trait to enable downcasting from trait objects of it.
118///
119/// See: <https://lucumr.pocoo.org/2022/1/7/as-any-hack/>
120pub trait BhErrorAny: BhError + Any {
121    /// Return `self` as [Any] type.
122    fn as_any(&self) -> &dyn Any;
123}
124
125impl<E: BhError> BhErrorAny for E {
126    fn as_any(&self) -> &dyn Any {
127        self
128    }
129}
130
131// This impl covers all boxed error types, including `dyn BhError`
132impl<E: BhError + ?Sized> BhError for Box<E> {}
133
134/// Error containing type-erased [`BhError`].
135///
136/// Needs to use the [`BhErrorAny`] subtrait so that [`BhErrorAny::as_any`] would be available on
137/// the internal error type.
138pub type ErrorDyn = Error<Box<dyn BhErrorAny>>;
139
140trait KnownError: std::error::Error + Send + Sync {
141    fn as_err(&self) -> &(dyn std::error::Error + 'static);
142}
143
144impl<T> KnownError for Error<T>
145where
146    T: BhError,
147{
148    fn as_err(&self) -> &(dyn std::error::Error + 'static) {
149        self
150    }
151}
152
153enum ErrorSource {
154    KnownError(Box<dyn KnownError>),
155    ForeignError(Box<dyn std::error::Error + Send + Sync>),
156}
157
158/// A struct that should be used for all errors in our projects.
159///
160/// It wraps specific errors created to model different error groups. Those errors should all
161/// implement the [`BhError`] trait in order to be compatible. They should not implement the
162/// [`std::error::Error`] trait themselves, it will be handled by this [`Error`] struct.
163///
164/// This [`Error`] struct should be used whenever the [`std::result::Result`] is used as the return
165/// type of the function/method, to model the returned error.  For convenience, we provide a type
166/// alias [`Result`], so that you don't have to explicitly wrap your [`BhError`] into [`Error`].
167///
168/// This wrapper automatically keeps track of the whole error chain, as well as the context
169/// assigned to the error, which might elaborate on the error specifics. It also handles all the
170/// error displays.
171pub struct Error<E>
172where
173    E: BhError,
174{
175    /// The concrete error variant.
176    pub error: E,
177    /// The optional context of the error.
178    context: Vec<Box<dyn std::fmt::Display + Send + Sync>>,
179    /// The error source, to be able to backtrace errors.
180    source: Option<ErrorSource>,
181}
182
183/// The [`std::result::Result`] wrapper that wraps the error object into [`Error`].
184pub type Result<T, E> = std::result::Result<T, Error<E>>;
185
186impl<E> Error<E>
187where
188    E: BhError,
189{
190    /// Create a root error (i.e. it does not have a source) and log a warning.
191    ///
192    /// It should be used in places where an error happened for the first time.  E.g. within `if`
193    /// or `if let` constructs.
194    ///
195    /// Do *not* use this method to propagate another error, because the whole error chain will be
196    /// lost.  If you want to propagate an error (i.e. track the source error), use either a method
197    /// from the [traits::ForeignError] or the [traits::PropagateError].
198    #[track_caller]
199    pub fn root(error: E) -> Self {
200        Self {
201            error,
202            context: Vec::new(),
203            source: None,
204        }
205        .log_warn(*std::panic::Location::caller())
206    }
207
208    /// Creates an error from its source, which is a foreign (unknown) error.
209    ///
210    /// The method should stay private, as it should not be used from the library/service code.
211    fn from_foreign_source<S>(error: E, source: S) -> Self
212    where
213        S: std::error::Error + Send + Sync + 'static,
214    {
215        Self {
216            error,
217            context: Vec::new(),
218            source: Some(ErrorSource::ForeignError(Box::new(source))),
219        }
220    }
221
222    /// Creates an error from its source, which is a known error.
223    ///
224    /// The method should stay private, as it should not be used from the library/service code.
225    fn from_known_source<S>(error: E, source: S) -> Self
226    where
227        S: KnownError + 'static,
228    {
229        Self {
230            error,
231            context: Vec::new(),
232            source: Some(ErrorSource::KnownError(Box::new(source))),
233        }
234    }
235
236    /// Creates an error from its source, which is a foreign (unknown) error.  Here, a concrete
237    /// error type is not known at compile time.
238    ///
239    /// The method should stay private, as it should not be used from the library/service code.
240    fn from_foreign_boxed_source(
241        error: E,
242        source: Box<dyn std::error::Error + Send + Sync>,
243    ) -> Self {
244        Self {
245            error,
246            context: Vec::new(),
247            source: Some(ErrorSource::ForeignError(source)),
248        }
249    }
250
251    /// Adds additional context to the error and returns it. It should be used to enrich the error
252    /// with further explanations.
253    ///
254    /// The method takes ownership of `self` so that the method can be chained.
255    ///
256    /// Context can be added multiple times and all the contexts will be saved to the error.
257    pub fn ctx<C>(mut self, context: C) -> Self
258    where
259        C: std::fmt::Display + Send + Sync + 'static,
260    {
261        self.context.push(Box::new(context));
262        self
263    }
264
265    /// Type-erases the error, making it wrap a `dyn BhError` trait object.
266    ///
267    /// This is mostly useful when implementing traits which must be object-safe but the type of
268    /// possible errors is not statically known; in such cases, [ErrorDyn] can be used instead of
269    /// associated error types.
270    pub fn erased(self) -> ErrorDyn {
271        Error {
272            error: Box::new(self.error),
273            context: self.context,
274            source: self.source,
275        }
276    }
277}
278
279impl ErrorDyn {
280    /// Tries downcasting the contained `dyn BhErrorAny` to `E`.
281    ///
282    /// This is mostly useful when trying to recover the concrete error type to match on after it
283    /// had been erased previously.
284    pub fn downcast_ref_inner<E: BhError>(&self) -> Option<&E> {
285        // The `.deref()` is important, since we want to call `.as_any()` with
286        // `&dyn BhErrorAny`, not `&Box<dyn BhErrorAny>`, which would compile
287        // but give `None` when downcasting to
288        // https://lucumr.pocoo.org/2022/1/7/as-any-hack/
289        self.error.deref().as_any().downcast_ref()
290    }
291}
292
293// Make the Error a std::error::Error type.
294impl<E> std::error::Error for Error<E>
295where
296    E: BhError,
297{
298    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
299        self.source.as_ref().map(|source| match source {
300            ErrorSource::KnownError(source) => source.as_ref().as_err(),
301            // "as _" here denotes casting to the output type, i.e. from
302            // (Error + Send + Sync) to (Error + 'static). It is the same as
303            // using "as &(dyn std::error::Error + 'static)".
304            ErrorSource::ForeignError(source) => source.as_ref() as _,
305        })
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use std::error::Error as _;
312
313    use super::*;
314
315    #[derive(Debug, PartialEq)]
316    enum DummyError {
317        SystemError,
318        UsageError,
319    }
320
321    impl std::fmt::Display for DummyError {
322        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323            match self {
324                Self::SystemError => write!(f, "SystemError"),
325                Self::UsageError => write!(f, "UsageError"),
326            }
327        }
328    }
329
330    impl BhError for DummyError {}
331
332    #[test]
333    fn test_root() {
334        let error = Error::root(DummyError::SystemError);
335
336        assert_eq!(error.error, DummyError::SystemError);
337        assert!(error.source.is_none());
338    }
339
340    #[test]
341    fn test_from_foreign_source() {
342        let error_sys = Error::root(DummyError::SystemError);
343        let error_us = Error::from_foreign_source(DummyError::UsageError, error_sys);
344
345        assert_eq!(error_us.error, DummyError::UsageError);
346        assert!(matches!(
347            error_us.source,
348            Some(ErrorSource::ForeignError(_))
349        ));
350    }
351
352    #[test]
353    fn test_from_known_source() {
354        let error_sys = Error::root(DummyError::SystemError);
355        let error_us = Error::from_known_source(DummyError::UsageError, error_sys);
356
357        assert_eq!(error_us.error, DummyError::UsageError);
358        assert!(matches!(error_us.source, Some(ErrorSource::KnownError(_))));
359    }
360
361    #[test]
362    fn test_ctx() {
363        let error = Error::root(DummyError::UsageError).ctx("Dummy first context");
364
365        assert_eq!(error.error, DummyError::UsageError);
366        assert!(error.source.is_none());
367        assert!(error
368            .context
369            .iter()
370            .map(ToString::to_string)
371            .any(|ctx| &ctx == "Dummy first context"));
372
373        let error = error.ctx("Dummy second context");
374
375        assert_eq!(error.error, DummyError::UsageError);
376        assert!(error.source.is_none());
377        let ctx_vec: Vec<String> = error.context.iter().map(ToString::to_string).collect();
378        assert!(ctx_vec.contains(&String::from("Dummy first context")));
379        assert!(ctx_vec.contains(&String::from("Dummy second context")));
380    }
381
382    #[test]
383    fn test_source() {
384        let error = Error {
385            error: DummyError::SystemError,
386            context: Vec::new(),
387            source: None,
388        };
389        assert!(error.source().is_none());
390
391        let error = Error {
392            error: DummyError::UsageError,
393            context: Vec::new(),
394            source: Some(ErrorSource::ForeignError(Box::new(error))),
395        };
396        assert!(error.source().is_some());
397
398        let error = Error {
399            error: DummyError::SystemError,
400            context: Vec::new(),
401            source: Some(ErrorSource::KnownError(Box::new(error))),
402        };
403        assert!(error.source().is_some());
404    }
405
406    #[test]
407    fn test_downcast_erased() {
408        let error = Error {
409            error: DummyError::SystemError,
410            context: Vec::new(),
411            source: None,
412        };
413
414        let erased_error = error.erased();
415        let downcast_error = erased_error.downcast_ref_inner::<DummyError>();
416
417        assert_eq!(downcast_error, Some(&DummyError::SystemError));
418    }
419}