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}