# `Bourse.Error`
[🔗](https://github.com/ZenHive/bourse/blob/main/lib/bourse/error.ex#L1)

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 after `retry_after` ms
- `:network_error` - Connection or timeout issue
- `:exchange_not_available` - Exchange down, on maintenance, or market closed
- `:invalid_nonce` - Nonce/timestamp window drift (client clock skew, concurrent
  signers, recv-window jitter); safe to retry after re-sync

### Non-recoverable (require intervention)
- `:authentication_error` - API key/secret rejected (credential failure)
- `: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 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

# `error_type`

```elixir
@type error_type() ::
  :rate_limit_exceeded
  | :network_error
  | :exchange_not_available
  | :invalid_nonce
  | :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
```

# `retry_class`

```elixir
@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 (generic `exchange_error`)

# `t`

```elixir
@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()
}
```

# `access_restricted`

```elixir
@spec access_restricted(keyword()) :: t()
```

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.

# `authentication_error`

```elixir
@spec authentication_error(keyword()) :: t()
```

Creates an authentication error.

# `bad_request`

```elixir
@spec bad_request(keyword()) :: t()
```

Creates a bad request error.

# `bad_symbol`

```elixir
@spec bad_symbol(keyword()) :: t()
```

Creates a bad symbol error.

# `circuit_open`

```elixir
@spec circuit_open(keyword()) :: t()
```

Creates a circuit breaker open error.

# `cloudflare_challenge`

```elixir
@spec cloudflare_challenge(keyword()) :: t()
```

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.

# `exchange_error`

```elixir
@spec exchange_error(
  String.t(),
  keyword()
) :: t()
```

Creates a generic exchange error.

Use this for errors that don't fit other categories.

# `exchange_not_available`

```elixir
@spec exchange_not_available(keyword()) :: t()
```

Creates an exchange not available error.

# `from_spec_class`

```elixir
@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

# `from_spec_class`

```elixir
@spec from_spec_class(String.t(), %{optional(String.t()) =&gt; [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

# `insufficient_funds`

```elixir
@spec insufficient_funds(keyword()) :: t()
```

Creates an insufficient funds error.

# `invalid_nonce`

```elixir
@spec invalid_nonce(keyword()) :: t()
```

Creates an invalid-nonce / timestamp-window error.

Transient client clock skew, concurrent nonce races, and recv-window
jitter — not credential rejection. `retry_class` is `:network`.

# `invalid_order`

```elixir
@spec invalid_order(keyword()) :: t()
```

Creates an invalid order error.

# `invalid_parameters`

```elixir
@spec invalid_parameters(keyword()) :: t()
```

Creates an invalid parameters error.

# `market_closed`

```elixir
@spec market_closed(keyword()) :: t()
```

Creates a market closed error.

# `network_error`

```elixir
@spec network_error(keyword()) :: t()
```

Creates a network error.

# `non_recoverable_types`

```elixir
@spec non_recoverable_types() :: [error_type()]
```

Returns all non-recoverable error types.

# `not_supported`

```elixir
@spec not_supported(keyword()) :: t()
```

Creates a not supported error.

# `operation_failed`

```elixir
@spec operation_failed(keyword()) :: t()
```

Creates an operation failed error.

# `order_not_found`

```elixir
@spec order_not_found(keyword()) :: t()
```

Creates an order not found error.

# `permission_denied`

```elixir
@spec permission_denied(keyword()) :: t()
```

Creates a permission denied error.

# `rate_limit_exceeded`

```elixir
@spec rate_limit_exceeded(keyword()) :: t()
```

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

# `recoverable?`

```elixir
@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)

# `recoverable_types`

```elixir
@spec recoverable_types() :: [error_type()]
```

Returns all recoverable error types.

# `retry_class`

```elixir
@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

# `retry_class_mapping`

```elixir
@spec retry_class_mapping() :: %{required(error_type()) =&gt; retry_class()}
```

Returns the canonical error_type → retry bucket map.

# `should_retry?`

```elixir
@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_class_mapping`

```elixir
@spec spec_class_mapping() :: %{required(String.t()) =&gt; error_type()}
```

Returns the full spec class to error type mapping.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
