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, recoverability, and the upstream retry classification bucket.
Error Types
Recoverable (can retry automatically)
:rate_limit_exceeded- Too many requests, retry afterretry_afterms: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 (seecodeandmessage)
Example
case Bourse.HTTP.request(exchange, :post, "/v5/order/create", params: params) do
{:ok, response} -> handle_response(response)
{:error, %Bourse.Error{type: :insufficient_funds}} -> notify_low_balance()
{:error, %Bourse.Error{type: :rate_limit_exceeded, retry_after: ms}} -> Process.sleep(ms)
{:error, %Bourse.Error{} = err} -> Logger.error("Exchange error: #{err.message}")
end
Summary
Types
Upstream (Bourse Phase 13) retry classification bucket for an error.
Functions
Creates an access restricted error.
Creates an authentication error.
Creates a bad request error.
Creates a bad symbol error.
Creates a circuit breaker open error.
Creates a Cloudflare challenge error.
Creates a generic exchange error.
Creates an exchange not available error.
Maps a Bourse spec exception class to an error type atom.
Maps a Bourse spec exception class to an error type, walking the upstream class hierarchy when the class is not directly mapped.
Creates an insufficient funds error.
Creates an invalid order error.
Creates an invalid parameters error.
Creates a market closed error.
Creates a network error.
Returns all non-recoverable error types.
Creates a not supported error.
Creates an operation failed error.
Creates an order not found error.
Creates a permission denied error.
Creates a rate limit exceeded error.
Returns the recoverability classification for an error type.
Returns all recoverable error types.
Returns the canonical retry classification bucket for an error type.
Returns the canonical error_type → retry bucket map.
Returns whether an error (or retry bucket) is worth retrying.
Returns the full spec class to error type mapping.
Types
@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 retry_class() ::
:rate_limit | :network | :server_busy | :auth | :non_retryable | nil
Upstream (Bourse Phase 13) retry classification bucket for an error.
Adopted from the v4 spec's errors.retry_classification. Refines the
coarse recoverable? boolean into the canonical retry semantics:
:rate_limit— back off and retry (handled by the rate limiter):network— transient connectivity/timeout; safe to retry:server_busy— exchange unavailable/maintenance; safe to retry:auth— credential/permission failure; do not retry without intervention:non_retryable— deterministic rejection (bad params, no funds, …)nil— unclassified (genericexchange_error)
@type t() :: %Bourse.Error{ __exception__: term(), code: String.t() | integer() | nil, exchange: String.t() | nil, hints: [String.t()], http_status: non_neg_integer() | nil, message: String.t(), raw: map() | binary() | nil, recoverable: boolean() | nil, retry_after: non_neg_integer() | nil, retry_class: retry_class(), type: error_type() }
Functions
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.
Creates an authentication error.
Creates a bad request error.
Creates a bad symbol error.
Creates a circuit breaker open error.
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.
Creates a generic exchange error.
Use this for errors that don't fit other categories.
Creates an exchange not available error.
@spec from_spec_class(String.t()) :: error_type()
Maps a Bourse 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(), %{optional(String.t()) => [String.t()]}) :: error_type()
Maps a Bourse spec exception class to an error type, walking the upstream class hierarchy when the class is not directly mapped.
ancestors is the per-exchange errors.class_hierarchy.ancestors map
(%{class => [ancestor, ...]}, nearest first). When class_name has no
direct mapping, the nearest mapped ancestor wins; only when neither the
class nor any ancestor is known does it fall back to :exchange_error.
This keeps the client robust to new upstream classes (e.g. AddressPending
resolves through InvalidAddress, ChecksumError through InvalidNonce)
without enumerating every leaf.
Examples
iex> Bourse.Error.from_spec_class("InsufficientFunds", %{})
:insufficient_funds
iex> ancestors = %{"AddressPending" => ["InvalidAddress", "ExchangeError", "BaseError"]}
iex> Bourse.Error.from_spec_class("AddressPending", ancestors)
:bad_request
iex> Bourse.Error.from_spec_class("TotallyUnknown", %{})
:exchange_error
Creates an insufficient funds error.
Creates an invalid order error.
Creates an invalid parameters error.
Creates a market closed error.
Creates a network error.
@spec non_recoverable_types() :: [error_type()]
Returns all non-recoverable error types.
Creates a not supported error.
Creates an operation failed error.
Creates an order not found error.
Creates a permission denied error.
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 recoverable?(error_type()) :: boolean() | nil
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_types() :: [error_type()]
Returns all recoverable error types.
@spec retry_class(error_type()) :: retry_class()
Returns the canonical retry classification bucket for an error type.
Returns nil for the generic :exchange_error (and any unknown type) —
the retry semantics are genuinely unknown for an unmapped error.
Examples
iex> Bourse.Error.retry_class(:rate_limit_exceeded)
:rate_limit
iex> Bourse.Error.retry_class(:authentication_error)
:auth
iex> Bourse.Error.retry_class(:exchange_error)
nil
@spec retry_class_mapping() :: %{required(error_type()) => retry_class()}
Returns the canonical error_type → retry bucket map.
@spec should_retry?(t() | retry_class()) :: boolean()
Returns whether an error (or retry bucket) is worth retrying.
Retryable buckets are :rate_limit, :network, and :server_busy. An
:auth, :non_retryable, or unclassified (nil) error returns false.
Examples
iex> Bourse.Error.should_retry?(:server_busy)
true
iex> Bourse.Error.should_retry?(:auth)
false
iex> Bourse.Error.should_retry?(%Bourse.Error{type: :rate_limit_exceeded, message: "", retry_class: :rate_limit})
true
@spec spec_class_mapping() :: %{required(String.t()) => error_type()}
Returns the full spec class to error type mapping.