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}