Skip to main content

tower_resilience_core/
error.rs

1//! Common error types for tower-resilience patterns.
2//!
3//! This module provides [`ResilienceError`], a unified error type that eliminates
4//! the need for manual `From` trait implementations when composing multiple resilience
5//! layers.
6//!
7//! # The Problem
8//!
9//! When using multiple resilience layers (bulkhead, circuit breaker, rate limiter, etc.),
10//! you typically need to write repetitive `From` trait implementations:
11//!
12//! ```rust,ignore
13//! // Without ResilienceError: ~80 lines of boilerplate for 4 layers
14//! impl From<BulkheadError> for ServiceError { /* ... */ }
15//! impl From<CircuitBreakerError> for ServiceError { /* ... */ }
16//! impl From<RateLimiterError> for ServiceError { /* ... */ }
17//! impl From<TimeLimiterError> for ServiceError { /* ... */ }
18//! ```
19//!
20//! # The Solution
21//!
22//! Use [`ResilienceError<E>`] as your service error type:
23//!
24//! ```rust
25//! use tower_resilience_core::ResilienceError;
26//!
27//! // Your application error
28//! #[derive(Debug, Clone)]
29//! enum AppError {
30//!     DatabaseDown,
31//!     InvalidRequest,
32//! }
33//!
34//! impl std::fmt::Display for AppError {
35//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36//!         match self {
37//!             AppError::DatabaseDown => write!(f, "Database down"),
38//!             AppError::InvalidRequest => write!(f, "Invalid request"),
39//!         }
40//!     }
41//! }
42//!
43//! impl std::error::Error for AppError {}
44//!
45//! // That's it! Zero From implementations needed
46//! type ServiceError = ResilienceError<AppError>;
47//! ```
48//!
49//! # Benefits
50//!
51//! - **Zero boilerplate**: No manual `From` implementations
52//! - **Works with any number of layers**: Add or remove layers without touching error code
53//! - **Rich error context**: Layer names, counts, durations included
54//! - **Application errors preserved**: Wrapped in `Application` variant
55//! - **Convenient helpers**: `is_timeout()`, `is_rate_limited()`, etc.
56//!
57//! # Pattern Matching
58//!
59//! ```rust
60//! use tower_resilience_core::ResilienceError;
61//! use std::time::Duration;
62//!
63//! # #[derive(Debug)]
64//! # struct AppError;
65//! # impl std::fmt::Display for AppError {
66//! #     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) }
67//! # }
68//! # impl std::error::Error for AppError {}
69//! fn handle_error(error: ResilienceError<AppError>) {
70//!     match error {
71//!         ResilienceError::Timeout { layer } => {
72//!             eprintln!("Timeout in {}", layer);
73//!         }
74//!         ResilienceError::CircuitOpen { name } => {
75//!             eprintln!("Circuit breaker {:?} is open", name);
76//!         }
77//!         ResilienceError::BulkheadFull { concurrent_calls, max_concurrent } => {
78//!             eprintln!("Bulkhead full: {}/{}", concurrent_calls, max_concurrent);
79//!         }
80//!         ResilienceError::RateLimited { retry_after } => {
81//!             eprintln!("Rate limited, retry after {:?}", retry_after);
82//!         }
83//!         ResilienceError::InstanceEjected { name } => {
84//!             eprintln!("Instance '{}' ejected by outlier detection", name);
85//!         }
86//!         ResilienceError::Application(app_err) => {
87//!             eprintln!("Application error: {}", app_err);
88//!         }
89//!     }
90//! }
91//! ```
92//!
93//! # Helper Methods
94//!
95//! ```rust
96//! use tower_resilience_core::ResilienceError;
97//!
98//! # #[derive(Debug)]
99//! # struct AppError;
100//! # impl std::fmt::Display for AppError {
101//! #     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) }
102//! # }
103//! # impl std::error::Error for AppError {}
104//! # let error: ResilienceError<AppError> = ResilienceError::Timeout { layer: "test" };
105//! if error.is_timeout() {
106//!     // Handle timeout from any layer
107//! } else if error.is_application() {
108//!     let app_error = error.application_error().unwrap();
109//!     // Handle application-specific error
110//! }
111//! ```
112//!
113//! # When to Use
114//!
115//! **Use `ResilienceError<E>` when:**
116//! - Building new services with multiple resilience layers
117//! - You want zero boilerplate error handling
118//! - Standard error categorization is sufficient
119//! - You're prototyping or want to move fast
120//!
121//! **Use manual `From` implementations when:**
122//! - You need very specific error semantics
123//! - Different layers require different recovery strategies
124//! - Integrating with legacy error types
125//! - You need specialized error logging per layer
126//!
127//! # Migration
128//!
129//! Existing code using manual `From` implementations continues to work.
130//! New code can adopt `ResilienceError<E>` incrementally:
131//!
132//! ```rust,ignore
133//! // Old code (still works)
134//! type ServiceError = MyCustomError; // with manual From impls
135//!
136//! // New code (zero boilerplate)
137//! type ServiceError = ResilienceError<MyAppError>;
138//! ```
139
140use std::fmt;
141use std::time::Duration;
142
143/// A common error type that wraps all resilience layer errors.
144///
145/// This allows users to compose multiple resilience patterns without
146/// writing any error conversion code. Each resilience layer error automatically
147/// converts into the appropriate `ResilienceError` variant.
148///
149/// # Type Parameters
150///
151/// - `E`: The application-specific error type from the wrapped service
152///
153/// # Examples
154///
155/// ```
156/// use tower_resilience_core::ResilienceError;
157/// use std::time::Duration;
158///
159/// // Your application error
160/// #[derive(Debug)]
161/// enum AppError {
162///     Network(String),
163///     InvalidData,
164/// }
165///
166/// impl std::fmt::Display for AppError {
167///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168///         match self {
169///             AppError::Network(msg) => write!(f, "Network: {}", msg),
170///             AppError::InvalidData => write!(f, "Invalid data"),
171///         }
172///     }
173/// }
174///
175/// impl std::error::Error for AppError {}
176///
177/// // Use ResilienceError<AppError> throughout your resilience stack
178/// type ServiceError = ResilienceError<AppError>;
179///
180/// // No From implementations needed - just use the error type!
181/// fn handle_error(err: ServiceError) {
182///     match err {
183///         ResilienceError::Timeout { layer } => {
184///             println!("Timeout in {}", layer);
185///         }
186///         ResilienceError::CircuitOpen { .. } => {
187///             println!("Circuit breaker is open");
188///         }
189///         ResilienceError::Application(app_err) => {
190///             println!("Application error: {}", app_err);
191///         }
192///         _ => println!("Other resilience error"),
193///     }
194/// }
195/// ```
196#[derive(Debug, Clone)]
197pub enum ResilienceError<E> {
198    /// A timeout occurred (from TimeLimiter or Bulkhead).
199    Timeout {
200        /// The layer that timed out (e.g., "time_limiter", "bulkhead")
201        layer: &'static str,
202    },
203
204    /// Circuit breaker is open, call rejected.
205    CircuitOpen {
206        /// Circuit breaker name (if configured)
207        name: Option<String>,
208    },
209
210    /// Bulkhead is at capacity, call rejected.
211    BulkheadFull {
212        /// Current number of concurrent calls
213        concurrent_calls: usize,
214        /// Maximum allowed concurrent calls
215        max_concurrent: usize,
216    },
217
218    /// Rate limiter rejected the call.
219    RateLimited {
220        /// How long to wait before retrying (if available)
221        retry_after: Option<Duration>,
222    },
223
224    /// An instance was ejected by outlier detection.
225    InstanceEjected {
226        /// The name of the ejected instance
227        name: String,
228    },
229
230    /// The underlying application service returned an error.
231    Application(E),
232}
233
234impl<E> fmt::Display for ResilienceError<E>
235where
236    E: fmt::Display,
237{
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        match self {
240            ResilienceError::Timeout { layer } => write!(f, "Timeout in {}", layer),
241            ResilienceError::CircuitOpen { name } => match name {
242                Some(n) => write!(f, "Circuit breaker '{}' is open", n),
243                None => write!(f, "Circuit breaker is open"),
244            },
245            ResilienceError::BulkheadFull {
246                concurrent_calls,
247                max_concurrent,
248            } => write!(f, "Bulkhead full ({}/{})", concurrent_calls, max_concurrent),
249            ResilienceError::RateLimited { retry_after } => match retry_after {
250                Some(d) => write!(f, "Rate limited, retry after {:?}", d),
251                None => write!(f, "Rate limited"),
252            },
253            ResilienceError::InstanceEjected { name } => {
254                write!(f, "Instance '{}' ejected by outlier detection", name)
255            }
256            ResilienceError::Application(e) => write!(f, "Application error: {}", e),
257        }
258    }
259}
260
261impl<E> std::error::Error for ResilienceError<E> where E: std::error::Error {}
262
263// Note: From implementations for each resilience layer error are provided
264// by the individual crates (bulkhead, circuitbreaker, etc.) to avoid
265// circular dependencies.
266
267/// Trait for converting service errors into [`ResilienceError<E>`].
268///
269/// This is used by [`ResilienceErrorLayer`](crate::error_layer::ResilienceErrorLayer)
270/// to convert each layer's error type into the unified `ResilienceError<E>`.
271///
272/// A blanket implementation is provided for any type that implements
273/// `Into<ResilienceError<E>>`, which covers all tower-resilience service error types.
274///
275/// You typically don't need to implement this trait directly.
276pub trait IntoResilienceError<E> {
277    /// Convert this error into a `ResilienceError<E>`.
278    fn into_resilience_error(self) -> ResilienceError<E>;
279}
280
281/// Blanket implementation: any type with `Into<ResilienceError<E>>` gets this for free.
282///
283/// This covers:
284/// - `ResilienceError<E>` (identity, via `From<T> for T`)
285/// - All service error types like `BulkheadServiceError<E>`, `CircuitBreakerError<E>`, etc.
286impl<T, E> IntoResilienceError<E> for T
287where
288    T: Into<ResilienceError<E>>,
289{
290    fn into_resilience_error(self) -> ResilienceError<E> {
291        self.into()
292    }
293}
294
295impl<E> ResilienceError<E> {
296    /// Returns `true` if this is a timeout error.
297    pub fn is_timeout(&self) -> bool {
298        matches!(self, ResilienceError::Timeout { .. })
299    }
300
301    /// Returns `true` if this is a circuit breaker error.
302    pub fn is_circuit_open(&self) -> bool {
303        matches!(self, ResilienceError::CircuitOpen { .. })
304    }
305
306    /// Returns `true` if this is a bulkhead error.
307    pub fn is_bulkhead_full(&self) -> bool {
308        matches!(self, ResilienceError::BulkheadFull { .. })
309    }
310
311    /// Returns `true` if this is a rate limiter error.
312    pub fn is_rate_limited(&self) -> bool {
313        matches!(self, ResilienceError::RateLimited { .. })
314    }
315
316    /// Returns `true` if this is an instance ejection error from outlier detection.
317    pub fn is_instance_ejected(&self) -> bool {
318        matches!(self, ResilienceError::InstanceEjected { .. })
319    }
320
321    /// Returns `true` if this is an application error.
322    pub fn is_application(&self) -> bool {
323        matches!(self, ResilienceError::Application(_))
324    }
325
326    /// Extracts the application error, if this is an `Application` variant.
327    pub fn application_error(self) -> Option<E> {
328        match self {
329            ResilienceError::Application(e) => Some(e),
330            _ => None,
331        }
332    }
333
334    /// Maps the application error using a function.
335    ///
336    /// # Examples
337    ///
338    /// ```
339    /// use tower_resilience_core::ResilienceError;
340    ///
341    /// let err: ResilienceError<String> = ResilienceError::Application("error".to_string());
342    /// let mapped: ResilienceError<usize> = err.map_application(|s| s.len());
343    /// assert_eq!(mapped.application_error(), Some(5));
344    /// ```
345    pub fn map_application<F, T>(self, f: F) -> ResilienceError<T>
346    where
347        F: FnOnce(E) -> T,
348    {
349        match self {
350            ResilienceError::Timeout { layer } => ResilienceError::Timeout { layer },
351            ResilienceError::CircuitOpen { name } => ResilienceError::CircuitOpen { name },
352            ResilienceError::BulkheadFull {
353                concurrent_calls,
354                max_concurrent,
355            } => ResilienceError::BulkheadFull {
356                concurrent_calls,
357                max_concurrent,
358            },
359            ResilienceError::RateLimited { retry_after } => {
360                ResilienceError::RateLimited { retry_after }
361            }
362            ResilienceError::InstanceEjected { name } => ResilienceError::InstanceEjected { name },
363            ResilienceError::Application(e) => ResilienceError::Application(f(e)),
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[derive(Debug, Clone)]
373    struct TestError;
374
375    impl fmt::Display for TestError {
376        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377            write!(f, "test error")
378        }
379    }
380
381    impl std::error::Error for TestError {}
382
383    /// Compile-time assertion that ResilienceError is Send + Sync + 'static
384    /// when the inner error type is Send + Sync + 'static.
385    /// This is required for compatibility with tower's BoxError.
386    const _: () = {
387        const fn assert_send_sync_static<T: Send + Sync + 'static>() {}
388        assert_send_sync_static::<ResilienceError<TestError>>();
389    };
390
391    #[test]
392    fn test_into_box_error() {
393        let err: ResilienceError<TestError> = ResilienceError::Timeout { layer: "test" };
394        let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(err);
395        assert!(boxed.to_string().contains("Timeout"));
396    }
397
398    #[test]
399    fn test_application_error_into_box_error() {
400        let err: ResilienceError<TestError> = ResilienceError::Application(TestError);
401        let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(err);
402        assert!(boxed.to_string().contains("test error"));
403    }
404}