saluki_error/
lib.rs

1//! A trait object-based error type based on `anyhow` for ergonomic error handling.
2//!
3//! This crate is a thin wrapper around `anyhow` that provides a trait object-based error type, `GenericError`, along
4//! with helper extension traits and macros for ergonomically converting errors into `GenericError`.
5//!
6//! This crate is still interoperable with `anyhow`, but is meant to be used as part of the comprehensive set of
7//! Saluki-specific crates so that engineers don't need to necessarily know or care deeply about which specific
8//! third-party crate to use for error handling.
9#![deny(warnings)]
10#![deny(missing_docs)]
11
12/// A wrapper around a dynamic error type.
13///
14/// `GenericError` works a lot like `Box<dyn std::error::Error>`, but with these differences:
15///
16/// - `GenericError` requires that the error is `Send`, `Sync`, and `'static`.
17/// - `GenericError` guarantees that a backtrace is available, even if the underlying error type doesn't provide one.
18/// - `GenericError` is represented as a narrow pointer—exactly one word in size instead of two.
19pub type GenericError = anyhow::Error;
20
21/// Macro for constructing a generic error.
22///
23/// The resulting value evaluates to [`GenericError`], and can be construct from a string literal, a format string (with
24/// arguments accepted, in the same order as `std::format!`), or a value which implements `Debug` and `Display`, such as
25/// an existing error that implements `std::error::Error`.
26///
27/// When the value given implements `std::error::Error`, the source of the existing error value will be used as the
28/// source of the error created by this macro.
29#[macro_export]
30macro_rules! generic_error {
31    // This macro forwards to the [`anyhow::anyhow`] macro, and is intended to be used in place of that macro. We simply
32    // use our own macro, instead of re-exporting it, so that we can provide better documentation that isn't
33    // `anyhow`-specific.
34    ($msg:literal $(,)?) => { $crate::_anyhow!($msg) };
35    ($err:expr $(,)?) => { $crate::_anyhow!($err) };
36    ($fmt:expr, $($arg:tt)*) => { $crate::_anyhow!($fmt, $($arg)*) };
37}
38
39use std::fmt::Display;
40
41#[doc(hidden)]
42pub use anyhow::anyhow as _anyhow;
43
44mod private {
45    pub trait Sealed {}
46
47    impl<T, E> Sealed for Result<T, E> {}
48}
49
50/// Helper methods for providing context on errors.
51///
52/// Slightly different than `anyhow::Context` as the methods on this trait are named to avoid conflicting with
53/// similarly named methods provide by `snafu`.
54pub trait ErrorContext<T, E>: private::Sealed {
55    /// Wrap the error value with additional context.
56    fn error_context<C>(self, context: C) -> Result<T, GenericError>
57    where
58        C: Display + Send + Sync + 'static;
59
60    /// Wrap the error value with additional context that's evaluated lazily only once an error does occur.
61    fn with_error_context<C, F>(self, f: F) -> Result<T, GenericError>
62    where
63        C: Display + Send + Sync + 'static,
64        F: FnOnce() -> C;
65}
66
67impl<T, E> ErrorContext<T, E> for Result<T, E>
68where
69    Result<T, E>: anyhow::Context<T, E>,
70{
71    fn error_context<C>(self, context: C) -> Result<T, GenericError>
72    where
73        C: Display + Send + Sync + 'static,
74    {
75        <Self as anyhow::Context<T, E>>::context(self, context)
76    }
77
78    fn with_error_context<C, F>(self, context: F) -> Result<T, GenericError>
79    where
80        C: Display + Send + Sync + 'static,
81        F: FnOnce() -> C,
82    {
83        <Self as anyhow::Context<T, E>>::with_context(self, context)
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use std::cell::Cell;
90    use std::fmt;
91
92    use super::ErrorContext as _;
93
94    /// A leaf error with no source of its own.
95    #[derive(Debug)]
96    struct LeafError;
97
98    impl fmt::Display for LeafError {
99        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100            write!(f, "leaf failure")
101        }
102    }
103
104    impl std::error::Error for LeafError {}
105
106    /// An error that carries [`LeafError`] as its source.
107    #[derive(Debug)]
108    struct WrappingError {
109        source: LeafError,
110    }
111
112    impl fmt::Display for WrappingError {
113        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114            write!(f, "wrapping failure")
115        }
116    }
117
118    impl std::error::Error for WrappingError {
119        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
120            Some(&self.source)
121        }
122    }
123
124    #[test]
125    fn generic_error_from_string_literal_uses_message_as_display() {
126        let err = generic_error!("boom");
127        assert_eq!(err.to_string(), "boom");
128    }
129
130    #[test]
131    fn generic_error_from_format_string_interpolates_arguments() {
132        let err = generic_error!("value {} out of range {}", 42, "here");
133        assert_eq!(err.to_string(), "value 42 out of range here");
134    }
135
136    #[test]
137    fn generic_error_from_error_value_forwards_source_chain() {
138        // Documented contract: when the value implements `std::error::Error`, the source of that error
139        // becomes part of the constructed `GenericError`'s chain, so the root cause is the wrapped
140        // error's own source rather than the wrapper itself.
141        let err = generic_error!(WrappingError { source: LeafError });
142        assert_eq!(err.to_string(), "wrapping failure");
143        assert_eq!(err.root_cause().to_string(), "leaf failure");
144    }
145
146    #[test]
147    fn error_context_passes_through_ok_unchanged() {
148        let result: Result<i32, std::io::Error> = Ok(7);
149        let contextualized = result.error_context("this context should never appear");
150        assert_eq!(contextualized.unwrap(), 7);
151    }
152
153    #[test]
154    fn error_context_wraps_err_with_context_and_preserves_source() {
155        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
156        let result: Result<(), std::io::Error> = Err(io_err);
157
158        let err = result.error_context("while loading config").unwrap_err();
159        assert_eq!(err.to_string(), "while loading config");
160        assert_eq!(err.root_cause().to_string(), "permission denied");
161    }
162
163    #[test]
164    fn with_error_context_is_not_evaluated_on_ok() {
165        let called = Cell::new(false);
166        let result: Result<i32, std::io::Error> = Ok(1);
167
168        let contextualized = result.with_error_context(|| {
169            called.set(true);
170            "lazy context"
171        });
172
173        assert_eq!(contextualized.unwrap(), 1);
174        assert!(!called.get(), "context closure must not run on the Ok path");
175    }
176
177    #[test]
178    fn with_error_context_is_evaluated_and_applied_on_err() {
179        let called = Cell::new(false);
180        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
181        let result: Result<(), std::io::Error> = Err(io_err);
182
183        let err = result
184            .with_error_context(|| {
185                called.set(true);
186                "lazy context"
187            })
188            .unwrap_err();
189
190        assert!(called.get(), "context closure must run on the Err path");
191        assert_eq!(err.to_string(), "lazy context");
192        assert_eq!(err.root_cause().to_string(), "missing");
193    }
194}