Skip to main content

narwhal_core/
error.rs

1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, Error>;
4
5/// Boxed driver-level cause carried alongside a high-level
6/// classification. `Send + Sync + 'static` so the error can cross task
7/// boundaries (tokio spawn, mpsc channels) without contortions.
8pub type Source = Box<dyn std::error::Error + Send + Sync + 'static>;
9
10/// Error returned from the core abstractions and from driver implementations.
11///
12/// M1: each high-level variant now has a `*WithSource` sibling that
13/// preserves the underlying driver error via `thiserror`'s `#[source]`
14/// chain. Callers that need to discriminate driver-specific conditions
15/// (e.g. `tokio_postgres::Error::is_closed()`) can downcast via
16/// `std::error::Error::source()` and `Any::downcast_ref` without
17/// requiring narwhal-core to depend on every driver crate.
18///
19/// The string-only variants are retained for backwards compatibility
20/// with drivers that don't yet carry the source through; both render
21/// identically via `Display`, so log lines and user-facing messages
22/// are unchanged.
23#[derive(Debug, Error)]
24#[non_exhaustive]
25pub enum Error {
26    #[error("connection failed: {0}")]
27    Connection(String),
28
29    #[error("connection failed: {msg}")]
30    ConnectionWithSource {
31        msg: String,
32        #[source]
33        source: Source,
34    },
35
36    #[error("authentication failed")]
37    Authentication,
38
39    #[error("query failed: {0}")]
40    Query(String),
41
42    #[error("query failed: {msg}")]
43    QueryWithSource {
44        msg: String,
45        #[source]
46        source: Source,
47    },
48
49    #[error("driver `{0}` is not registered")]
50    UnknownDriver(String),
51
52    #[error("unsupported type: {0}")]
53    UnsupportedType(String),
54
55    #[error("feature not supported by this driver: {0}")]
56    Unsupported(String),
57
58    #[error("schema error: {0}")]
59    Schema(String),
60
61    #[error("configuration error: {0}")]
62    Config(String),
63
64    #[error("configuration error: {msg}")]
65    ConfigWithSource {
66        msg: String,
67        #[source]
68        source: Source,
69    },
70
71    #[error("operation was cancelled")]
72    Cancelled,
73
74    #[error("io error: {0}")]
75    Io(#[from] std::io::Error),
76
77    #[error("{0}")]
78    Other(String),
79}
80
81impl Error {
82    pub fn other(msg: impl Into<String>) -> Self {
83        Self::Other(msg.into())
84    }
85
86    pub fn unsupported(msg: impl Into<String>) -> Self {
87        Self::Unsupported(msg.into())
88    }
89
90    /// Build a connection error that preserves the underlying driver
91    /// error in the `source()` chain.
92    pub fn connection_with(msg: impl Into<String>, source: impl Into<Source>) -> Self {
93        Self::ConnectionWithSource {
94            msg: msg.into(),
95            source: source.into(),
96        }
97    }
98
99    /// Build a query error that preserves the underlying driver error
100    /// in the `source()` chain.
101    pub fn query_with(msg: impl Into<String>, source: impl Into<Source>) -> Self {
102        Self::QueryWithSource {
103            msg: msg.into(),
104            source: source.into(),
105        }
106    }
107
108    /// Build a configuration error that preserves the underlying parse
109    /// / validation error in the `source()` chain.
110    pub fn config_with(msg: impl Into<String>, source: impl Into<Source>) -> Self {
111        Self::ConfigWithSource {
112            msg: msg.into(),
113            source: source.into(),
114        }
115    }
116
117    /// Walk the `std::error::Error::source()` chain looking for a
118    /// concrete driver error type. Convenience wrapper around the
119    /// repeated downcast pattern.
120    pub fn find_source<T: std::error::Error + 'static>(&self) -> Option<&T> {
121        let mut current: Option<&(dyn std::error::Error + 'static)> =
122            std::error::Error::source(self);
123        while let Some(err) = current {
124            if let Some(target) = err.downcast_ref::<T>() {
125                return Some(target);
126            }
127            current = err.source();
128        }
129        None
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[derive(Debug, Error)]
138    #[error("driver-specific boom: code={0}")]
139    struct FakeDriverError(u32);
140
141    #[test]
142    fn connection_with_preserves_source() {
143        let driver = FakeDriverError(42);
144        let err = Error::connection_with("failed to handshake", driver);
145        assert!(matches!(err, Error::ConnectionWithSource { .. }));
146        assert_eq!(err.to_string(), "connection failed: failed to handshake");
147        let found = err.find_source::<FakeDriverError>().expect("chain");
148        assert_eq!(found.0, 42);
149    }
150
151    #[test]
152    fn query_with_chain_is_walkable() {
153        let err = Error::query_with("select bombed", FakeDriverError(7));
154        let mut chain: Option<&(dyn std::error::Error + 'static)> = std::error::Error::source(&err);
155        let mut hops = 0;
156        while let Some(c) = chain {
157            hops += 1;
158            chain = c.source();
159        }
160        assert!(hops >= 1);
161    }
162
163    #[test]
164    fn legacy_string_variants_unchanged() {
165        // The existing tuple variants must keep working so drivers can
166        // migrate gradually.
167        let err = Error::Connection("plain".into());
168        assert_eq!(err.to_string(), "connection failed: plain");
169        assert!(std::error::Error::source(&err).is_none());
170    }
171}