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;