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}