Skip to main content

outbox_core/
error.rs

1//! Shared error type for every fallible operation in the crate.
2//!
3//! All public APIs return `Result<T, `[`OutboxError`]`>`. The error is a flat
4//! enum rather than a `Box<dyn Error>` so callers can reason about categories
5//! — infrastructure, database, broker, configuration — without downcasting.
6//! Display strings are deliberate and tested; they are part of the public
7//! contract because they surface directly in logs and propagate to
8//! integrators.
9
10use thiserror::Error;
11
12/// Error categories produced by the outbox crate.
13///
14/// The variants correspond to the layer that originated the failure. When a
15/// caller needs to decide whether to retry, the variant is usually enough —
16/// most transient conditions show up as [`InfrastructureError`](Self::InfrastructureError),
17/// [`DatabaseError`](Self::DatabaseError), or
18/// [`BrokerError`](Self::BrokerError); configuration and deduplication issues
19/// are terminal.
20#[derive(Debug, Error)]
21pub enum OutboxError {
22    /// Failure from surrounding infrastructure that is not the primary
23    /// database or broker (Redis, a notification channel, DNS, etc.). Usually
24    /// transient.
25    #[error("Infrastructure error: {0}")]
26    InfrastructureError(String),
27    /// The event's idempotency token has already been reserved by a prior
28    /// call. Returned by [`OutboxService::add_event`](crate::service::OutboxService::add_event)
29    /// when [`IdempotencyStorageProvider::try_reserve`](crate::idempotency::storage::IdempotencyStorageProvider::try_reserve)
30    /// reports the token as taken. Not retryable.
31    #[error("Duplicate error")]
32    DuplicateEvent,
33    /// Failure from the primary event store (for example SQL errors, pool
34    /// exhaustion, serialization conflicts). Often retryable by the caller.
35    #[error("Database error: {0}")]
36    DatabaseError(String),
37    /// Failure from the message transport (Kafka, Redis Streams, etc.) when
38    /// publishing an event.
39    #[error("Broker error: {0}")]
40    BrokerError(String),
41    /// Invalid or incomplete configuration discovered at wiring time —
42    /// typically raised by [`OutboxManagerBuilder::build`](crate::builder::OutboxManagerBuilder::build)
43    /// when a required collaborator is missing. Not retryable.
44    #[error("Config error: {0}")]
45    ConfigError(String),
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use rstest::rstest;
52
53    #[rstest]
54    fn display_infrastructure_error_includes_inner_message() {
55        let e = OutboxError::InfrastructureError("redis down".into());
56        assert_eq!(format!("{e}"), "Infrastructure error: redis down");
57    }
58
59    #[rstest]
60    fn display_duplicate_event_is_static_string() {
61        let e = OutboxError::DuplicateEvent;
62        assert_eq!(format!("{e}"), "Duplicate error");
63    }
64
65    #[rstest]
66    fn display_database_error_includes_inner_message() {
67        let e = OutboxError::DatabaseError("pk conflict".into());
68        assert_eq!(format!("{e}"), "Database error: pk conflict");
69    }
70
71    #[rstest]
72    fn display_broker_error_includes_inner_message() {
73        let e = OutboxError::BrokerError("kafka timeout".into());
74        assert_eq!(format!("{e}"), "Broker error: kafka timeout");
75    }
76
77    #[rstest]
78    fn display_config_error_includes_inner_message() {
79        let e = OutboxError::ConfigError("missing field".into());
80        assert_eq!(format!("{e}"), "Config error: missing field");
81    }
82
83    #[rstest]
84    fn std_error_trait_is_implemented() {
85        fn takes_error<E: std::error::Error + Send + Sync + 'static>(_: E) {}
86        takes_error(OutboxError::DuplicateEvent);
87    }
88}