Skip to main content

ccxt_core/error/
mod.rs

1//! # Error Handling for CCXT Rust
2//!
3//! This module provides a comprehensive, production-grade error handling system for the
4//! `ccxt-rust` library. It is designed following Rust community best practices and the
5//! principles outlined in the [Error Handling Project Group](https://blog.rust-lang.org/inside-rust/2021/07/01/What-the-error-handling-project-group-is-working-towards.html).
6//!
7//! ## Design Philosophy
8//!
9//! The error handling system is built around these core principles:
10//!
11//! 1. **Type Safety**: Strongly-typed errors using `thiserror` for compile-time guarantees
12//! 2. **API Stability**: All public enums use `#[non_exhaustive]` for forward compatibility
13//! 3. **Zero Panic**: No `unwrap()` or `expect()` on recoverable error paths
14//! 4. **Context Rich**: Full error chain support with context attachment
15//! 5. **Performance**: Optimized memory layout using `Cow<'static, str>` and `Box`
16//! 6. **Thread Safety**: All error types implement `Send + Sync + 'static`
17//! 7. **Observability**: Integration with `tracing` for structured logging
18//!
19//! ## Error Hierarchy
20//!
21//! ```text
22//! Error (main error type)
23//! ├── Exchange      - Exchange-specific API errors
24//! ├── Network       - Network/transport layer errors (via NetworkError)
25//! ├── Parse         - Response parsing errors (via ParseError)
26//! ├── Order         - Order management errors (via OrderError)
27//! ├── Authentication - API key/signature errors
28//! ├── RateLimit     - Rate limiting with retry information
29//! ├── Timeout       - Operation timeout
30//! ├── InvalidRequest - Invalid parameters
31//! ├── MarketNotFound - Unknown trading pair
32//! ├── WebSocket     - WebSocket communication errors
33//! └── Context       - Error with additional context
34//! ```
35//!
36//! ## Quick Start
37//!
38//! ### Basic Error Handling
39//!
40//! ```rust
41//! use ccxt_core::error::{Error, Result};
42//!
43//! fn fetch_price(symbol: &str) -> Result<f64> {
44//!     if symbol.is_empty() {
45//!         return Err(Error::invalid_request("Symbol cannot be empty"));
46//!     }
47//!     // ... fetch price logic
48//!     Ok(42000.0)
49//! }
50//! ```
51//!
52//! ### Adding Context to Errors
53//!
54//! ```rust
55//! use ccxt_core::error::{Error, Result, ContextExt};
56//!
57//! fn process_order(order_id: &str) -> Result<()> {
58//!     validate_order(order_id)
59//!         .context("Failed to validate order")?;
60//!
61//!     submit_order(order_id)
62//!         .with_context(|| format!("Failed to submit order {}", order_id))?;
63//!
64//!     Ok(())
65//! }
66//! # fn validate_order(_: &str) -> Result<()> { Ok(()) }
67//! # fn submit_order(_: &str) -> Result<()> { Ok(()) }
68//! ```
69//!
70//! ### Handling Specific Error Types
71//!
72//! ```rust
73//! use ccxt_core::error::{Error, NetworkError};
74//!
75//! fn handle_error(err: Error) {
76//!     // Check if error is retryable
77//!     if err.is_retryable() {
78//!         if let Some(duration) = err.retry_after() {
79//!             println!("Retry after {:?}", duration);
80//!         }
81//!     }
82//!
83//!     // Check for specific error types through context layers
84//!     if let Some(msg) = err.as_authentication() {
85//!         println!("Auth error: {}", msg);
86//!     }
87//!
88//!     // Get full error report
89//!     println!("Error report:\n{}", err.report());
90//! }
91//! ```
92//!
93//! ### Creating Exchange Errors
94//!
95//! ```rust
96//! use ccxt_core::error::Error;
97//!
98//! // Simple exchange error
99//! let err = Error::exchange("-1121", "Invalid symbol");
100//!
101//! // Exchange error with raw response data
102//! let err = Error::exchange_with_data(
103//!     "400",
104//!     "Bad Request",
105//!     serde_json::json!({"code": -1121, "msg": "Invalid symbol"})
106//! );
107//! ```
108//!
109//! ## Memory Optimization
110//!
111//! The `Error` enum is optimized to be ≤56 bytes on 64-bit systems:
112//!
113//! - Large variants (`Exchange`, `Network`, `Parse`, `Order`, `WebSocket`, `Context`)
114//!   are boxed to keep the enum size small
115//! - String fields use `Cow<'static, str>` to avoid allocation for static strings
116//! - Use `Error::authentication("static message")` for zero-allocation errors
117//! - Use `Error::authentication(format!("dynamic {}", value))` when needed
118//!
119//! ## Feature Flags
120//!
121//! - `backtrace`: Enable backtrace capture in `ExchangeErrorDetails` for debugging
122//!
123//! ## Integration with anyhow
124//!
125//! For application-level code, errors can be converted to `anyhow::Error`:
126//!
127//! ```rust
128//! use ccxt_core::error::Error;
129//!
130//! fn app_main() -> anyhow::Result<()> {
131//!     let result: Result<(), Error> = Err(Error::timeout("Operation timed out"));
132//!     result?; // Automatically converts to anyhow::Error
133//!     Ok(())
134//! }
135//! ```
136
137mod config;
138mod context;
139mod convert;
140mod details;
141mod network;
142mod order;
143mod parse;
144
145use std::borrow::Cow;
146use std::error::Error as StdError;
147use std::fmt;
148use std::time::Duration;
149use thiserror::Error;
150
151// Re-export all public types for backward compatibility
152pub use config::{ConfigValidationError, ValidationResult};
153pub use context::ContextExt;
154#[allow(deprecated)]
155pub use context::ErrorContext;
156pub use details::ExchangeErrorDetails;
157pub use network::NetworkError;
158pub use order::OrderError;
159pub use parse::ParseError;
160
161/// Result type alias for all CCXT operations.
162pub type Result<T> = std::result::Result<T, Error>;
163
164/// The primary error type for the `ccxt-rust` library.
165///
166/// Design constraints:
167/// - All large variants are boxed to keep enum size ≤ 56 bytes
168/// - Uses `Cow<'static, str>` for zero-allocation static strings
169/// - Verify with: `assert!(std::mem::size_of::<Error>() <= 56);`
170///
171/// # Example
172///
173/// ```rust
174/// use ccxt_core::error::Error;
175///
176/// let err = Error::authentication("Invalid API key");
177/// assert!(err.to_string().contains("Invalid API key"));
178/// ```
179#[derive(Error, Debug)]
180#[non_exhaustive]
181pub enum Error {
182    /// Exchange-specific errors returned by the exchange API.
183    /// Boxed to reduce enum size (`ExchangeErrorDetails` is large).
184    #[error("Exchange error: {0}")]
185    Exchange(Box<ExchangeErrorDetails>),
186
187    /// Network-related errors encapsulating transport layer issues.
188    /// Boxed to reduce enum size.
189    #[error("Network error: {0}")]
190    Network(Box<NetworkError>),
191
192    /// Authentication errors (invalid API key, signature, etc.).
193    #[error("Authentication error: {0}")]
194    Authentication(Cow<'static, str>),
195
196    /// Rate limit exceeded with optional retry information.
197    #[error("Rate limit exceeded: {message}")]
198    RateLimit {
199        /// Error message
200        message: Cow<'static, str>,
201        /// Optional duration to wait before retrying
202        retry_after: Option<Duration>,
203    },
204
205    /// Invalid request parameters.
206    #[error("Invalid request: {0}")]
207    InvalidRequest(Cow<'static, str>),
208
209    /// Order-related errors. Boxed to reduce enum size.
210    #[error("Order error: {0}")]
211    Order(Box<OrderError>),
212
213    /// Insufficient balance for an operation.
214    #[error("Insufficient balance: {0}")]
215    InsufficientBalance(Cow<'static, str>),
216
217    /// Invalid order format or parameters.
218    #[error("Invalid order: {0}")]
219    InvalidOrder(Cow<'static, str>),
220
221    /// Order not found on the exchange.
222    #[error("Order not found: {0}")]
223    OrderNotFound(Cow<'static, str>),
224
225    /// Market symbol not found or not supported.
226    #[error("Market not found: {0}")]
227    MarketNotFound(Cow<'static, str>),
228
229    /// Errors during response parsing. Boxed to reduce enum size.
230    #[error("Parse error: {0}")]
231    Parse(Box<ParseError>),
232
233    /// WebSocket communication errors.
234    /// Uses `Box<dyn StdError>` to preserve original error for downcast.
235    #[error("WebSocket error: {0}")]
236    WebSocket(#[source] Box<dyn StdError + Send + Sync + 'static>),
237
238    /// Operation timeout.
239    #[error("Timeout: {0}")]
240    Timeout(Cow<'static, str>),
241
242    /// Feature not implemented for this exchange.
243    #[error("Not implemented: {0}")]
244    NotImplemented(Cow<'static, str>),
245
246    /// Operation was cancelled.
247    ///
248    /// This error is returned when an operation is cancelled via a `CancellationToken`
249    /// or other cancellation mechanism. It indicates that the operation was intentionally
250    /// aborted and did not complete.
251    ///
252    /// # Example
253    ///
254    /// ```rust
255    /// use ccxt_core::error::Error;
256    ///
257    /// let err = Error::cancelled("WebSocket connection cancelled");
258    /// assert!(err.to_string().contains("cancelled"));
259    /// ```
260    #[error("Cancelled: {0}")]
261    Cancelled(Cow<'static, str>),
262
263    /// Resource exhausted error.
264    ///
265    /// This error is returned when a resource limit has been reached, such as
266    /// maximum number of WebSocket subscriptions, connection pool exhaustion,
267    /// or other capacity limits.
268    ///
269    /// # Example
270    ///
271    /// ```rust
272    /// use ccxt_core::error::Error;
273    ///
274    /// let err = Error::resource_exhausted("Maximum subscriptions (100) reached");
275    /// assert!(err.to_string().contains("Resource exhausted"));
276    /// ```
277    #[error("Resource exhausted: {0}")]
278    ResourceExhausted(Cow<'static, str>),
279
280    /// Configuration validation error.
281    ///
282    /// This error is returned when configuration parameters fail validation,
283    /// such as values being out of range or missing required fields.
284    /// Boxed to reduce enum size.
285    ///
286    /// # Example
287    ///
288    /// ```rust
289    /// use ccxt_core::error::{Error, ConfigValidationError};
290    ///
291    /// let err = Error::config_validation(ConfigValidationError::too_high("max_retries", 15, 10));
292    /// assert!(err.to_string().contains("Configuration error"));
293    /// ```
294    #[error("Configuration error: {0}")]
295    ConfigValidation(Box<ConfigValidationError>),
296
297    /// Error with additional context, preserving the error chain.
298    #[error("{context}")]
299    Context {
300        /// Context message describing what operation failed
301        context: String,
302        /// The underlying error
303        #[source]
304        source: Box<Error>,
305    },
306}
307
308impl Error {
309    // ==================== Constructor Methods ====================
310
311    /// Creates a new exchange error.
312    ///
313    /// # Example
314    ///
315    /// ```rust
316    /// use ccxt_core::error::Error;
317    ///
318    /// let err = Error::exchange("400", "Bad Request");
319    /// ```
320    pub fn exchange(code: impl Into<String>, message: impl Into<String>) -> Self {
321        Self::Exchange(Box::new(ExchangeErrorDetails::new(code, message)))
322    }
323
324    /// Creates a new exchange error with raw response data.
325    pub fn exchange_with_data(
326        code: impl Into<String>,
327        message: impl Into<String>,
328        data: serde_json::Value,
329    ) -> Self {
330        Self::Exchange(Box::new(ExchangeErrorDetails::with_data(
331            code, message, data,
332        )))
333    }
334
335    /// Creates a new rate limit error with optional retry duration.
336    /// Accepts both `&'static str` (zero allocation) and `String`.
337    ///
338    /// # Example
339    ///
340    /// ```rust
341    /// use ccxt_core::error::Error;
342    /// use std::time::Duration;
343    ///
344    /// // Zero allocation (static string):
345    /// let err = Error::rate_limit("Too many requests", Some(Duration::from_secs(60)));
346    ///
347    /// // Allocation (dynamic string):
348    /// let err = Error::rate_limit(format!("Rate limit: {}", 429), None);
349    /// ```
350    pub fn rate_limit(
351        message: impl Into<Cow<'static, str>>,
352        retry_after: Option<Duration>,
353    ) -> Self {
354        Self::RateLimit {
355            message: message.into(),
356            retry_after,
357        }
358    }
359
360    /// Creates an authentication error.
361    /// Accepts both `&'static str` (zero allocation) and `String`.
362    pub fn authentication(msg: impl Into<Cow<'static, str>>) -> Self {
363        Self::Authentication(msg.into())
364    }
365
366    /// Creates a generic error (for backwards compatibility).
367    pub fn generic(msg: impl Into<Cow<'static, str>>) -> Self {
368        Self::InvalidRequest(msg.into())
369    }
370
371    /// Creates a network error from a message.
372    pub fn network(msg: impl Into<String>) -> Self {
373        Self::Network(Box::new(NetworkError::ConnectionFailed(msg.into())))
374    }
375
376    /// Creates a market not found error.
377    /// Accepts both `&'static str` (zero allocation) and `String`.
378    pub fn market_not_found(symbol: impl Into<Cow<'static, str>>) -> Self {
379        Self::MarketNotFound(symbol.into())
380    }
381
382    /// Creates a not implemented error.
383    /// Accepts both `&'static str` (zero allocation) and `String`.
384    pub fn not_implemented(feature: impl Into<Cow<'static, str>>) -> Self {
385        Self::NotImplemented(feature.into())
386    }
387
388    /// Creates a cancelled error.
389    ///
390    /// Use this when an operation is cancelled via a `CancellationToken` or
391    /// other cancellation mechanism.
392    ///
393    /// Accepts both `&'static str` (zero allocation) and `String`.
394    ///
395    /// # Example
396    ///
397    /// ```rust
398    /// use ccxt_core::error::Error;
399    ///
400    /// // Zero allocation (static string):
401    /// let err = Error::cancelled("Operation cancelled by user");
402    ///
403    /// // Allocation (dynamic string):
404    /// let err = Error::cancelled(format!("Connection {} cancelled", "ws-1"));
405    /// ```
406    pub fn cancelled(msg: impl Into<Cow<'static, str>>) -> Self {
407        Self::Cancelled(msg.into())
408    }
409
410    /// Creates a resource exhausted error.
411    ///
412    /// Use this when a resource limit has been reached, such as maximum
413    /// WebSocket subscriptions, connection pool exhaustion, or other
414    /// capacity limits.
415    ///
416    /// Accepts both `&'static str` (zero allocation) and `String`.
417    ///
418    /// # Example
419    ///
420    /// ```rust
421    /// use ccxt_core::error::Error;
422    ///
423    /// // Zero allocation (static string):
424    /// let err = Error::resource_exhausted("Maximum subscriptions reached");
425    ///
426    /// // Allocation (dynamic string):
427    /// let err = Error::resource_exhausted(format!("Maximum subscriptions ({}) reached", 100));
428    /// ```
429    pub fn resource_exhausted(msg: impl Into<Cow<'static, str>>) -> Self {
430        Self::ResourceExhausted(msg.into())
431    }
432
433    /// Creates a configuration validation error.
434    ///
435    /// Use this when configuration parameters fail validation.
436    ///
437    /// # Example
438    ///
439    /// ```rust
440    /// use ccxt_core::error::{Error, ConfigValidationError};
441    ///
442    /// let err = Error::config_validation(ConfigValidationError::too_high("max_retries", 15, 10));
443    /// assert!(err.to_string().contains("Configuration error"));
444    /// ```
445    pub fn config_validation(err: ConfigValidationError) -> Self {
446        Self::ConfigValidation(Box::new(err))
447    }
448
449    /// Creates an invalid request error.
450    pub fn invalid_request(msg: impl Into<Cow<'static, str>>) -> Self {
451        Self::InvalidRequest(msg.into())
452    }
453
454    /// Creates an invalid argument error (alias for `invalid_request`).
455    pub fn invalid_argument(msg: impl Into<Cow<'static, str>>) -> Self {
456        Self::InvalidRequest(msg.into())
457    }
458
459    /// Creates a bad symbol error (alias for `invalid_request`).
460    pub fn bad_symbol(symbol: impl Into<String>) -> Self {
461        let s = symbol.into();
462        Self::InvalidRequest(Cow::Owned(format!("Bad symbol: {s}")))
463    }
464
465    /// Creates an insufficient balance error.
466    pub fn insufficient_balance(msg: impl Into<Cow<'static, str>>) -> Self {
467        Self::InsufficientBalance(msg.into())
468    }
469
470    /// Creates a timeout error.
471    pub fn timeout(msg: impl Into<Cow<'static, str>>) -> Self {
472        Self::Timeout(msg.into())
473    }
474
475    /// Creates a WebSocket error from a message string.
476    pub fn websocket(msg: impl Into<String>) -> Self {
477        Self::WebSocket(Box::new(SimpleError(msg.into())))
478    }
479
480    /// Creates a WebSocket error from any error type.
481    pub fn websocket_error<E: StdError + Send + Sync + 'static>(err: E) -> Self {
482        Self::WebSocket(Box::new(err))
483    }
484
485    // ==================== Context Methods ====================
486
487    /// Attaches context to an existing error.
488    ///
489    /// # Example
490    ///
491    /// ```rust
492    /// use ccxt_core::error::Error;
493    ///
494    /// let err = Error::network("Connection refused")
495    ///     .context("Failed to fetch ticker for BTC/USDT");
496    /// ```
497    #[must_use]
498    pub fn context(self, context: impl Into<String>) -> Self {
499        Self::Context {
500            context: context.into(),
501            source: Box::new(self),
502        }
503    }
504
505    // ==================== Chain Traversal Methods ====================
506
507    /// Internal helper: creates an iterator that traverses the error chain.
508    /// Automatically penetrates Context layers.
509    fn iter_chain(&self) -> impl Iterator<Item = &Error> {
510        std::iter::successors(Some(self), |err| match err {
511            Error::Context { source, .. } => Some(source.as_ref()),
512            _ => None,
513        })
514    }
515
516    /// Returns the root cause of the error, skipping Context layers.
517    #[must_use]
518    pub fn root_cause(&self) -> &Error {
519        self.iter_chain().last().unwrap_or(self)
520    }
521
522    /// Finds a specific error variant in the chain (penetrates Context layers).
523    /// Useful for handling wrapped errors without manual unwrapping.
524    pub fn find_variant<F>(&self, matcher: F) -> Option<&Error>
525    where
526        F: Fn(&Error) -> bool,
527    {
528        self.iter_chain().find(|e| matcher(e))
529    }
530
531    /// Generates a detailed error report with the full chain.
532    ///
533    /// # Example
534    ///
535    /// ```rust
536    /// use ccxt_core::error::Error;
537    ///
538    /// let err = Error::network("Connection refused")
539    ///     .context("Failed to fetch ticker");
540    /// println!("{}", err.report());
541    /// // Output:
542    /// // Failed to fetch ticker
543    /// // Caused by: Network error: Connection failed: Connection refused
544    /// ```
545    #[must_use]
546    pub fn report(&self) -> String {
547        use std::fmt::Write;
548        let mut report = String::new();
549        report.push_str(&self.to_string());
550
551        let mut current: Option<&(dyn StdError + 'static)> = self.source();
552        while let Some(err) = current {
553            let _ = write!(report, "\nCaused by: {err}");
554            current = err.source();
555        }
556        report
557    }
558
559    // ==================== Helper Methods (Context Penetrating) ====================
560
561    /// Checks if this error is retryable (penetrates Context layers).
562    ///
563    /// Returns `true` for:
564    /// - `NetworkError::Timeout`
565    /// - `NetworkError::ConnectionFailed`
566    /// - `RateLimit`
567    /// - `Timeout`
568    #[must_use]
569    pub fn is_retryable(&self) -> bool {
570        match self {
571            Error::Network(ne) => matches!(
572                ne.as_ref(),
573                NetworkError::Timeout | NetworkError::ConnectionFailed(_)
574            ),
575            Error::RateLimit { .. } | Error::Timeout(_) => true,
576            Error::Context { source, .. } => source.is_retryable(),
577            _ => false,
578        }
579    }
580
581    /// Returns the retry delay if this is a rate limit error (penetrates Context layers).
582    #[must_use]
583    pub fn retry_after(&self) -> Option<Duration> {
584        match self {
585            Error::RateLimit { retry_after, .. } => *retry_after,
586            Error::Context { source, .. } => source.retry_after(),
587            _ => None,
588        }
589    }
590
591    /// Checks if this is a rate limit error (penetrates Context layers).
592    /// Returns the message and optional retry duration.
593    #[must_use]
594    pub fn as_rate_limit(&self) -> Option<(&str, Option<Duration>)> {
595        match self {
596            Error::RateLimit {
597                message,
598                retry_after,
599            } => Some((message.as_ref(), *retry_after)),
600            Error::Context { source, .. } => source.as_rate_limit(),
601            _ => None,
602        }
603    }
604
605    /// Checks if this is an authentication error (penetrates Context layers).
606    /// Returns the error message.
607    #[must_use]
608    pub fn as_authentication(&self) -> Option<&str> {
609        match self {
610            Error::Authentication(msg) => Some(msg.as_ref()),
611            Error::Context { source, .. } => source.as_authentication(),
612            _ => None,
613        }
614    }
615
616    /// Checks if this is a cancelled error (penetrates Context layers).
617    /// Returns the error message.
618    ///
619    /// # Example
620    ///
621    /// ```rust
622    /// use ccxt_core::error::Error;
623    ///
624    /// let err = Error::cancelled("Operation cancelled");
625    /// assert_eq!(err.as_cancelled(), Some("Operation cancelled"));
626    ///
627    /// // Works through context layers
628    /// let wrapped = err.context("Wrapped error");
629    /// assert_eq!(wrapped.as_cancelled(), Some("Operation cancelled"));
630    /// ```
631    #[must_use]
632    pub fn as_cancelled(&self) -> Option<&str> {
633        match self {
634            Error::Cancelled(msg) => Some(msg.as_ref()),
635            Error::Context { source, .. } => source.as_cancelled(),
636            _ => None,
637        }
638    }
639
640    /// Checks if this is a resource exhausted error (penetrates Context layers).
641    /// Returns the error message.
642    ///
643    /// # Example
644    ///
645    /// ```rust
646    /// use ccxt_core::error::Error;
647    ///
648    /// let err = Error::resource_exhausted("Maximum subscriptions reached");
649    /// assert_eq!(err.as_resource_exhausted(), Some("Maximum subscriptions reached"));
650    ///
651    /// // Works through context layers
652    /// let wrapped = err.context("Wrapped error");
653    /// assert_eq!(wrapped.as_resource_exhausted(), Some("Maximum subscriptions reached"));
654    /// ```
655    #[must_use]
656    pub fn as_resource_exhausted(&self) -> Option<&str> {
657        match self {
658            Error::ResourceExhausted(msg) => Some(msg.as_ref()),
659            Error::Context { source, .. } => source.as_resource_exhausted(),
660            _ => None,
661        }
662    }
663
664    /// Checks if this is a configuration validation error (penetrates Context layers).
665    /// Returns a reference to the `ConfigValidationError`.
666    ///
667    /// # Example
668    ///
669    /// ```rust
670    /// use ccxt_core::error::{Error, ConfigValidationError};
671    ///
672    /// let err = Error::config_validation(ConfigValidationError::too_high("max_retries", 15, 10));
673    /// assert!(err.as_config_validation().is_some());
674    ///
675    /// // Works through context layers
676    /// let wrapped = err.context("Wrapped error");
677    /// assert!(wrapped.as_config_validation().is_some());
678    /// ```
679    #[must_use]
680    pub fn as_config_validation(&self) -> Option<&ConfigValidationError> {
681        match self {
682            Error::ConfigValidation(err) => Some(err.as_ref()),
683            Error::Context { source, .. } => source.as_config_validation(),
684            _ => None,
685        }
686    }
687
688    /// Attempts to downcast the WebSocket error to a specific type.
689    #[must_use]
690    pub fn downcast_websocket<T: StdError + 'static>(&self) -> Option<&T> {
691        match self {
692            Error::WebSocket(e) => e.downcast_ref::<T>(),
693            Error::Context { source, .. } => source.downcast_websocket(),
694            _ => None,
695        }
696    }
697}
698
699/// A simple error type for wrapping string messages.
700/// Used internally for WebSocket errors created from strings.
701#[derive(Debug)]
702struct SimpleError(String);
703
704impl fmt::Display for SimpleError {
705    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
706        write!(f, "{}", self.0)
707    }
708}
709
710impl StdError for SimpleError {}
711
712#[cfg(test)]
713mod tests;