> ## Documentation Index
> Fetch the complete documentation index at: https://docs.limitless.exchange/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling & Retry

> Retry logic and error handling with the Go SDK

## Overview

The SDK provides a typed error hierarchy, a generic retry function with exponential backoff, a `RetryableClient` wrapper, and pluggable logging — all following idiomatic Go patterns.

## Error types

All API errors implement the `error` interface and can be inspected using `errors.As()`:

```go theme={null}
import "errors"

_, err := marketFetcher.GetMarket(ctx, "invalid-slug")
if err != nil {
    var apiErr *limitless.APIError
    if errors.As(err, &apiErr) {
        fmt.Printf("Status: %d\n", apiErr.Status)
        fmt.Printf("Message: %s\n", apiErr.Message)
        fmt.Printf("URL: %s %s\n", apiErr.Method, apiErr.URL)
    }
}
```

### APIError

The base error type for all HTTP errors. Its `Error()` method returns a formatted string like `"API error 401 GET /path: message"`.

| Field     | Type              | Description                                        |
| --------- | ----------------- | -------------------------------------------------- |
| `Status`  | `int`             | HTTP status code (e.g. `400`, `401`, `429`, `500`) |
| `Message` | `string`          | Human-readable error message from the API          |
| `Data`    | `json.RawMessage` | Raw response body                                  |
| `URL`     | `string`          | Request URL path                                   |
| `Method`  | `string`          | HTTP method                                        |

### Specialized error types

The SDK provides three specialized error types that embed `APIError`:

| Type                   | HTTP Status  | Description                                           |
| ---------------------- | ------------ | ----------------------------------------------------- |
| `*ValidationError`     | `400`        | Bad request — invalid parameters or malformed payload |
| `*AuthenticationError` | `401`, `403` | Missing/invalid API key or insufficient permissions   |
| `*RateLimitError`      | `429`        | Rate limit exceeded                                   |

```go theme={null}
var rateLimitErr *limitless.RateLimitError
if errors.As(err, &rateLimitErr) {
    fmt.Println("Rate limited! Back off and retry.")
}

var authErr *limitless.AuthenticationError
if errors.As(err, &authErr) {
    fmt.Println("Check your API key.")
}

var validErr *limitless.ValidationError
if errors.As(err, &validErr) {
    fmt.Println("Invalid request:", validErr.Message)
}
```

### OrderValidationError

Client-side validation errors thrown before a request is made:

```go theme={null}
var orderErr *limitless.OrderValidationError
if errors.As(err, &orderErr) {
    fmt.Printf("Field %s: %s\n", orderErr.Field, orderErr.Message)
}
```

| Field     | Type     | Description                                                    |
| --------- | -------- | -------------------------------------------------------------- |
| `Field`   | `string` | The field that failed validation (e.g. `"price"`, `"tokenId"`) |
| `Message` | `string` | Description of the validation failure                          |

### Common status codes

| Code  | Meaning               | Typical Cause                                                                                                                                                 |
| ----- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Bad Request           | Invalid order parameters, malformed payload                                                                                                                   |
| `401` | Unauthorized          | Missing or invalid API key                                                                                                                                    |
| `403` | Forbidden             | Insufficient permissions or geographic restriction                                                                                                            |
| `404` | Not Found             | Invalid market slug or order ID                                                                                                                               |
| `425` | Too Early             | Receive-window failure or maintenance block (`code`). Re-stamp and retry only for receive-window failures; refresh maintenance status for maintenance blocks. |
| `429` | Too Many Requests     | Rate limit exceeded                                                                                                                                           |
| `500` | Internal Server Error | Transient server-side failure                                                                                                                                 |

### Helper method

`APIError` provides an `IsAuthError()` method to quickly check for authentication issues:

```go theme={null}
if apiErr.IsAuthError() {
    fmt.Println("Authentication problem — check your API key")
}
```

## Raw HTTP responses

Every API-backed service method has a `...WithRawResponse` sibling that returns `*RawResult[T]`. Use it when you need the HTTP status, response headers (rate-limit metadata, request IDs, `Content-Type`), or the exact response bytes alongside the decoded value. The base method (without the suffix) is unchanged and still returns just the decoded value.

The raw variant still returns the same typed errors (`*APIError` and its subclasses) on any response with status `>= 400`. It only exposes the raw response on success.

```go theme={null}
import "github.com/limitless-labs-group/limitless-exchange-go-sdk/limitless"

// Default: decoded value only.
markets, err := marketFetcher.GetActiveMarkets(ctx, nil)

// Opt in to the raw response.
result, err := marketFetcher.GetActiveMarketsWithRawResponse(ctx, nil)
if err != nil {
    return err
}

fmt.Println("HTTP", result.Raw.Status)                                     // 200
fmt.Println("rate limit", result.Raw.Headers.Get("X-RateLimit-Remaining"))
fmt.Println("markets returned:", len(result.Data.Data))                    // decoded value
// result.Raw.Body holds the original JSON body as json.RawMessage.
```

### RawResult\[T]

| Field  | Type           | Description                                                            |
| ------ | -------------- | ---------------------------------------------------------------------- |
| `Data` | `T`            | The decoded response value, identical to what the base method returns. |
| `Raw`  | `*RawResponse` | The underlying HTTP response.                                          |

### RawResponse

| Field     | Type              | Description                 |
| --------- | ----------------- | --------------------------- |
| `Status`  | `int`             | HTTP status code.           |
| `Headers` | `http.Header`     | Response headers.           |
| `Body`    | `json.RawMessage` | The original response body. |

<Note>
  The `...WithRawResponse` variant is available on every API-backed method (markets, portfolio, orders, AMM, API tokens, delegated orders, partner accounts, and market pages). WebSocket subscriptions are not affected.
</Note>

## WithRetry (generic)

The SDK provides a generic `WithRetry` function that wraps any operation with configurable retry logic using Go generics:

```go theme={null}
market, err := limitless.WithRetry(ctx, func() (*limitless.Market, error) {
    return marketFetcher.GetMarket(ctx, "btc-above-100k-march-2025")
}, limitless.RetryConfig{
    StatusCodes:     []int{429, 500, 502, 503, 504},
    MaxRetries:      3,
    ExponentialBase: 2.0,
    MaxDelay:        60 * time.Second,
    OnRetry: func(attempt int, err error, delay time.Duration) {
        fmt.Printf("Retry %d after %s: %v\n", attempt, delay, err)
    },
})
```

### RetryConfig

| Field             | Type                              | Default                     | Description                                              |
| ----------------- | --------------------------------- | --------------------------- | -------------------------------------------------------- |
| `StatusCodes`     | `[]int`                           | `[429, 500, 502, 503, 504]` | HTTP status codes that trigger a retry                   |
| `MaxRetries`      | `int`                             | `3`                         | Maximum number of retry attempts                         |
| `Delays`          | `[]time.Duration`                 | `nil`                       | Fixed delays per attempt (overrides exponential backoff) |
| `ExponentialBase` | `float64`                         | `2.0`                       | Base for exponential backoff calculation                 |
| `MaxDelay`        | `time.Duration`                   | `60s`                       | Maximum delay between retries                            |
| `OnRetry`         | `func(int, error, time.Duration)` | `nil`                       | Callback invoked on each retry                           |

<Note>
  `WithRetry` only retries when the error is an `*APIError` whose `Status` is in the `StatusCodes` list. All other errors propagate immediately.
</Note>

### Fixed delays

Override exponential backoff with explicit per-attempt delays:

```go theme={null}
config := limitless.RetryConfig{
    StatusCodes: []int{429, 500},
    MaxRetries:  3,
    Delays:      []time.Duration{1 * time.Second, 2 * time.Second, 4 * time.Second},
}
```

## RetryableClient

For broader retry coverage, wrap your `HttpClient` with `RetryableClient`. This applies retry logic to **all** API calls made through the client:

```go theme={null}
client := limitless.NewHttpClient(
    limitless.WithHMACCredentials(limitless.HMACCredentials{
        TokenID: os.Getenv("LMTS_TOKEN_ID"),
        Secret:  os.Getenv("LMTS_TOKEN_SECRET"),
    }),
)
retryable := limitless.NewRetryableClient(client, limitless.RetryConfig{
    StatusCodes:     []int{429, 500, 502, 503, 504},
    MaxRetries:      3,
    ExponentialBase: 2.0,
})

// Call through retryable directly; transient failures are retried transparently
var market limitless.Market
err := retryable.Get(ctx, "/markets/btc-100k-weekly", &market)
```

The `RetryableClient` exposes the same request methods as `HttpClient` (`Get`, `GetRaw`, `Post`, `Patch`, `Delete`) and transparently retries on the configured status codes. Use these methods for raw requests that need retry coverage.

<Tip>
  Use `RetryableClient` for automated trading bots that need resilience against transient failures without wrapping every call in `WithRetry` individually.
</Tip>

## Debugging with ConsoleLogger

Enable verbose logging to trace requests, responses, and internal operations:

```go theme={null}
logger := limitless.NewConsoleLogger(limitless.LogLevelDebug)
client := limitless.NewHttpClient(limitless.WithLogger(logger))
```

At `LogLevelDebug`, the logger outputs:

* Request and response headers
* Venue cache hits and misses
* Full API response bodies
* Retry attempts and delays
* WebSocket connection state changes

<Accordion title="Example DEBUG output">
  ```
  [DEBUG] GET /markets/btc-above-100k-march-2025
  [DEBUG] Headers: {lmts-api-key: lmts_***, Content-Type: application/json}
  [DEBUG] Response 200 in 142ms
  [DEBUG] Venue cached for btc-above-100k-march-2025: exchange=0xA1b2... adapter=0xD4e5...
  [DEBUG] POST /orders
  [DEBUG] Response 429 in 38ms
  [DEBUG] Retry 1/3 after 1s — status 429
  [DEBUG] POST /orders
  [DEBUG] Response 200 in 156ms
  ```
</Accordion>

### Custom logger

Implement the `Logger` interface to plug in your own logging backend:

```go theme={null}
type Logger interface {
    Debug(msg string, meta ...map[string]any)
    Info(msg string, meta ...map[string]any)
    Warn(msg string, meta ...map[string]any)
    Error(msg string, err error, meta ...map[string]any)
}
```

<Note>
  The `Error` method has an additional `err error` parameter compared to the other methods, allowing structured error logging.
</Note>

## Best practices

<Steps>
  <Step title="Always check errors">
    Every SDK method returns `error` as the last return value. Always check it:

    ```go theme={null}
    result, err := orderClient.CreateOrder(ctx, params)
    if err != nil {
        var apiErr *limitless.APIError
        if errors.As(err, &apiErr) {
            log.Printf("API error %d: %s", apiErr.Status, apiErr.Message)
        }
        return err
    }
    ```
  </Step>

  <Step title="Use typed error assertions">
    Use `errors.As()` to match specific error types and handle them differently:

    ```go theme={null}
    var rateLimitErr *limitless.RateLimitError
    if errors.As(err, &rateLimitErr) {
        // Back off and retry
    }
    var authErr *limitless.AuthenticationError
    if errors.As(err, &authErr) {
        // Check API key configuration
    }
    ```
  </Step>

  <Step title="Retry only transient errors">
    Limit retries to status codes like `429` (rate limit) and `5xx` (server errors). Do not retry `400` (bad request) or `401` (auth failure) as these require corrective action.
  </Step>

  <Step title="Use exponential backoff">
    The default `RetryConfig` uses exponential backoff with a base of 2.0. Avoid fixed short delays that could overwhelm the API during outages.
  </Step>

  <Step title="Pass context for cancellation">
    Always pass a `context.Context` to allow timeouts and cancellation:

    ```go theme={null}
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    market, err := marketFetcher.GetMarket(ctx, slug)
    ```
  </Step>

  <Step title="Use DEBUG logging during development">
    Enable `LogLevelDebug` to see the full request/response cycle. Switch to `LogLevelInfo` or `LogLevelWarn` in production to reduce noise.
  </Step>
</Steps>
