Current section
Files
Jump to
Current section
Files
lib/ccxt/error.ex
defmodule CCXT.Error do
@moduledoc """
Unified error types for exchange operations.
All exchange errors are normalized to this struct, providing consistent
error handling across every configured exchange. Each error carries its
type, the original exchange error code and message, and whether it's
recoverable.
## Error Types
### Recoverable (can retry automatically)
- `:rate_limit_exceeded` - Too many requests, retry after `retry_after` ms
- `:network_error` - Connection or timeout issue
- `:exchange_not_available` - Exchange down, on maintenance, or market closed
### Non-recoverable (require intervention)
- `:authentication_error` - API key/secret rejected or invalid nonce
- `:insufficient_funds` - Not enough funds for the operation
- `:invalid_order` - Order parameters rejected by exchange
- `:order_not_found` - Order ID does not exist
- `:bad_request` - Invalid request parameters
- `:bad_symbol` - Symbol not recognized by exchange
- `:permission_denied` - API key lacks permissions or account suspended
- `:access_restricted` - Geographic/IP block or wrong-URL HTML response (non-Cloudflare)
- `:cloudflare_challenge` - Cloudflare anti-bot challenge page (exchange reachable but requires browser/approved client)
- `:not_supported` - Method not supported by this exchange
- `:operation_failed` - Operation rejected or failed
- `:invalid_parameters` - Invalid request parameters (code bug)
- `:market_closed` - Market is not currently trading
- `:circuit_open` - Circuit breaker tripped due to consecutive failures
### Generic
- `:exchange_error` - Unmapped error (see `code` and `message`)
## Example
case CCXT.HTTP.request(exchange, :post, "/v5/order/create", params: params) do
{:ok, response} -> handle_response(response)
{:error, %CCXT.Error{type: :insufficient_funds}} -> notify_low_balance()
{:error, %CCXT.Error{type: :rate_limit_exceeded, retry_after: ms}} -> Process.sleep(ms)
{:error, %CCXT.Error{} = err} -> Logger.error("Exchange error: \#{err.message}")
end
"""
@type error_type ::
:rate_limit_exceeded
| :network_error
| :exchange_not_available
| :authentication_error
| :insufficient_funds
| :invalid_order
| :order_not_found
| :bad_request
| :bad_symbol
| :permission_denied
| :access_restricted
| :cloudflare_challenge
| :not_supported
| :operation_failed
| :invalid_parameters
| :market_closed
| :circuit_open
| :exchange_error
@type t :: %__MODULE__{
type: error_type(),
code: String.t() | integer() | nil,
http_status: non_neg_integer() | nil,
message: String.t(),
exchange: String.t() | nil,
retry_after: non_neg_integer() | nil,
raw: map() | nil,
hints: [String.t()],
recoverable: boolean() | nil
}
defexception [
:type,
:code,
:http_status,
:message,
:exchange,
:retry_after,
:raw,
:recoverable,
hints: []
]
@impl Exception
def message(%__MODULE__{type: type, message: msg, exchange: exchange}) do
prefix = if exchange, do: "[#{exchange}] ", else: ""
"#{prefix}#{type}: #{msg}"
end
# ===========================================================================
# Recoverability Classification
# ===========================================================================
@recoverable_types [:rate_limit_exceeded, :network_error, :exchange_not_available]
@non_recoverable_types [
:authentication_error,
:insufficient_funds,
:invalid_order,
:order_not_found,
:bad_request,
:bad_symbol,
:permission_denied,
:access_restricted,
:cloudflare_challenge,
:not_supported,
:operation_failed,
:invalid_parameters,
:market_closed,
:circuit_open
]
@doc """
Returns the recoverability classification for an error type.
- `true` — recoverable (can retry automatically)
- `false` — not recoverable (requires intervention)
- `nil` — unknown (generic exchange_error)
"""
@spec recoverable?(error_type()) :: boolean() | nil
def recoverable?(type) when type in @recoverable_types, do: true
def recoverable?(type) when type in @non_recoverable_types, do: false
def recoverable?(:exchange_error), do: nil
def recoverable?(_), do: nil
@doc "Returns all recoverable error types."
@spec recoverable_types() :: [error_type()]
def recoverable_types, do: @recoverable_types
@doc "Returns all non-recoverable error types."
@spec non_recoverable_types() :: [error_type()]
def non_recoverable_types, do: @non_recoverable_types
# ===========================================================================
# Spec Class Mapping
#
# Maps CCXT spec exception class names (e.g., "__function:AuthenticationError")
# to Elixir error atoms. Used by Exchange.new/2 to pre-process exception maps.
# ===========================================================================
@spec_class_mapping %{
"AuthenticationError" => :authentication_error,
"InvalidNonce" => :authentication_error,
"InsufficientFunds" => :insufficient_funds,
"InvalidOrder" => :invalid_order,
"OrderImmediatelyFillable" => :invalid_order,
"OrderNotFillable" => :invalid_order,
"DuplicateOrderId" => :invalid_order,
"OrderNotFound" => :order_not_found,
"CancelPending" => :order_not_found,
"BadRequest" => :bad_request,
"ArgumentsRequired" => :bad_request,
"BadResponse" => :bad_request,
"BadSymbol" => :bad_symbol,
"ContractUnavailable" => :bad_symbol,
"PermissionDenied" => :permission_denied,
"AccountSuspended" => :permission_denied,
"AccountNotEnabled" => :permission_denied,
"RateLimitExceeded" => :rate_limit_exceeded,
"DDoSProtection" => :rate_limit_exceeded,
"RequestTimeout" => :network_error,
"NetworkError" => :network_error,
"ExchangeNotAvailable" => :exchange_not_available,
"OnMaintenance" => :exchange_not_available,
"MarketClosed" => :exchange_not_available,
"ExchangeClosedByUser" => :exchange_not_available,
"RestrictedLocation" => :access_restricted,
"NotSupported" => :not_supported,
"OperationFailed" => :operation_failed,
"OperationRejected" => :operation_failed,
"ExchangeError" => :exchange_error,
"ManualInteractionNeeded" => :exchange_error,
"MarginModeAlreadySet" => :exchange_error,
"NoChange" => :exchange_error,
"InvalidAddress" => :bad_request
}
@doc """
Maps a CCXT spec exception class to an error type atom.
Accepts both raw class names and `__function:` prefixed strings from specs.
## Examples
from_spec_class("AuthenticationError")
#=> :authentication_error
from_spec_class("__function:InsufficientFunds")
#=> :insufficient_funds
from_spec_class("UnknownClass")
#=> :exchange_error
"""
@spec from_spec_class(String.t()) :: error_type()
def from_spec_class("__function:" <> class_name), do: from_spec_class(class_name)
def from_spec_class(class_name) when is_binary(class_name) do
Map.get(@spec_class_mapping, class_name, :exchange_error)
end
@doc "Returns the full spec class to error type mapping."
@spec spec_class_mapping() :: %{String.t() => error_type()}
def spec_class_mapping, do: @spec_class_mapping
# ===========================================================================
# Factory Functions
# ===========================================================================
@doc """
Creates a rate limit exceeded error.
## Options
- `:retry_after` - Milliseconds until retry is allowed
- `:exchange` - Exchange ID string
- `:raw` - Original error response from exchange
- `:hints` - List of debugging hint strings
"""
@spec rate_limit_exceeded(keyword()) :: t()
def rate_limit_exceeded(opts \\ []) do
build(:rate_limit_exceeded, "Rate limit exceeded", opts)
end
@doc "Creates a network error."
@spec network_error(keyword()) :: t()
def network_error(opts \\ []) do
build(:network_error, "Network error", opts)
end
@doc "Creates an exchange not available error."
@spec exchange_not_available(keyword()) :: t()
def exchange_not_available(opts \\ []) do
build(:exchange_not_available, "Exchange not available", opts)
end
@doc "Creates an authentication error."
@spec authentication_error(keyword()) :: t()
def authentication_error(opts \\ []) do
build(:authentication_error, "Invalid API credentials", opts)
end
@doc "Creates an insufficient funds error."
@spec insufficient_funds(keyword()) :: t()
def insufficient_funds(opts \\ []) do
build(:insufficient_funds, "Insufficient funds", opts)
end
@doc "Creates an invalid order error."
@spec invalid_order(keyword()) :: t()
def invalid_order(opts \\ []) do
build(:invalid_order, "Invalid order parameters", opts)
end
@doc "Creates an order not found error."
@spec order_not_found(keyword()) :: t()
def order_not_found(opts \\ []) do
build(:order_not_found, "Order not found", opts)
end
@doc "Creates a bad request error."
@spec bad_request(keyword()) :: t()
def bad_request(opts \\ []) do
build(:bad_request, "Bad request", opts)
end
@doc "Creates a bad symbol error."
@spec bad_symbol(keyword()) :: t()
def bad_symbol(opts \\ []) do
build(:bad_symbol, "Invalid symbol", opts)
end
@doc "Creates a permission denied error."
@spec permission_denied(keyword()) :: t()
def permission_denied(opts \\ []) do
build(:permission_denied, "Permission denied", opts)
end
@doc """
Creates an access restricted error.
Used when exchange returns HTML instead of JSON without Cloudflare
markers — typically a wrong URL/prefix, geo/IP block, or landing page.
Cloudflare challenges use `cloudflare_challenge/1` instead.
"""
@spec access_restricted(keyword()) :: t()
def access_restricted(opts \\ []) do
build(:access_restricted, "Access restricted - exchange returned HTML instead of JSON", opts)
end
@doc """
Creates a Cloudflare challenge error.
Used when the exchange is reachable but served a Cloudflare anti-bot
challenge page (e.g. "Just a moment..."). Inconclusive for integration
tests — the client is reaching the right host but needs a browser or
approved path to pass the challenge.
"""
@spec cloudflare_challenge(keyword()) :: t()
def cloudflare_challenge(opts \\ []) do
build(:cloudflare_challenge, "Cloudflare challenge - exchange requires browser/approved client", opts)
end
@doc "Creates a not supported error."
@spec not_supported(keyword()) :: t()
def not_supported(opts \\ []) do
build(:not_supported, "Method not supported by this exchange", opts)
end
@doc "Creates an operation failed error."
@spec operation_failed(keyword()) :: t()
def operation_failed(opts \\ []) do
build(:operation_failed, "Operation failed", opts)
end
@doc "Creates an invalid parameters error."
@spec invalid_parameters(keyword()) :: t()
def invalid_parameters(opts \\ []) do
build(:invalid_parameters, "Invalid request parameters", opts)
end
@doc "Creates a market closed error."
@spec market_closed(keyword()) :: t()
def market_closed(opts \\ []) do
build(:market_closed, "Market is closed", opts)
end
@doc "Creates a circuit breaker open error."
@spec circuit_open(keyword()) :: t()
def circuit_open(opts \\ []) do
build(:circuit_open, "Circuit breaker is open", opts)
end
@doc """
Creates a generic exchange error.
Use this for errors that don't fit other categories.
"""
@spec exchange_error(String.t(), keyword()) :: t()
def exchange_error(message, opts \\ []) do
build(:exchange_error, message, opts)
end
# ===========================================================================
# Private Helpers
# ===========================================================================
# Builds an error struct with consistent field population and auto-classified recoverability.
@spec build(error_type(), String.t(), keyword()) :: t()
defp build(type, default_message, opts) do
%__MODULE__{
type: type,
message: Keyword.get(opts, :message, default_message),
code: Keyword.get(opts, :code),
retry_after: Keyword.get(opts, :retry_after),
exchange: Keyword.get(opts, :exchange),
raw: Keyword.get(opts, :raw),
hints: Keyword.get(opts, :hints, []),
recoverable: recoverable?(type)
}
end
end