> ## 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 TypeScript SDK

## APIError class

The SDK throws all HTTP errors as `APIError` instances. Inspect the `status`, `message`, and `data` properties to determine what went wrong.

```typescript theme={null}
import { APIError } from '@limitless-exchange/sdk';

try {
  await orderClient.createOrder({ /* ... */ });
} catch (error) {
  if (error instanceof APIError) {
    console.error('Status:', error.status);   // HTTP status code
    console.error('Message:', error.message);  // Human-readable error
    console.error('Data:', error.data);        // Raw response body (if any)
  }
}
```

### APIError properties

| Property  | Type      | Description                                 |
| --------- | --------- | ------------------------------------------- |
| `status`  | `number`  | HTTP status code (400, 401, 429, 500, etc.) |
| `message` | `string`  | Error description from the server           |
| `data`    | `unknown` | Raw response body, useful for debugging     |

## Common status codes

| Code  | Meaning                                         | Action                                                                                                                                                                                                          |
| ----- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Bad request (invalid parameters)                | Fix request parameters                                                                                                                                                                                          |
| `401` | Unauthorized (invalid or missing credentials)   | Check your scoped API token (`LMTS_TOKEN_ID` / `LMTS_TOKEN_SECRET`)                                                                                                                                             |
| `403` | Forbidden (insufficient permissions)            | Verify account permissions                                                                                                                                                                                      |
| `404` | Not found (invalid slug or resource)            | Check market slug or resource ID                                                                                                                                                                                |
| `425` | Too Early — receive-window or maintenance block | If `error.data.code` is a trading-mode code, refresh maintenance status and do not retry the same blocked action. Otherwise, treat `POST /orders` failures as receive-window failures and re-stamp `timestamp`. |
| `429` | Rate limited                                    | Back off and retry with delay                                                                                                                                                                                   |
| `500` | Internal server error                           | Retry with exponential backoff                                                                                                                                                                                  |
| `502` | Bad gateway                                     | Retry with exponential backoff                                                                                                                                                                                  |
| `503` | Service unavailable                             | Retry with exponential backoff                                                                                                                                                                                  |
| `504` | Gateway timeout                                 | Retry with exponential backoff                                                                                                                                                                                  |

## Raw HTTP responses

Every API-backed service method accepts an opt-in `{ withRawResponse: true }` option that returns the decoded value alongside the underlying HTTP status, headers, and response body. Use it when you need to inspect status codes, response headers (rate limit metadata, request IDs, `content-type`), or the exact bytes the API returned. The default return shape is unchanged when the option is omitted or set to `false`.

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

```typescript theme={null}
import { SdkResponse } from '@limitless-exchange/sdk';

// Default: decoded value only.
const markets = await marketFetcher.getActiveMarkets();

// Opt in to the raw response.
const response: SdkResponse<ActiveMarketsResponse> =
  await marketFetcher.getActiveMarkets(undefined, { withRawResponse: true });

const { status, headers, data } = response.getRaw();
console.log('HTTP', status);                     // 200
console.log('rate limit', headers['x-ratelimit-remaining']);
console.log(response.data.data.length, 'markets'); // decoded value, same as the default return
```

### SdkResponse\<T>

| Property / method | Type                 | Description                                                          |
| ----------------- | -------------------- | -------------------------------------------------------------------- |
| `data`            | `T`                  | The decoded, typed value. Identical to what the base method returns. |
| `getRaw()`        | `HttpRawResponse<T>` | The underlying HTTP response (`status`, `headers`, `data`).          |

<Note>
  The option 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 wrapper

The SDK provides a `withRetry` utility function that wraps any async operation with configurable retry logic.

```typescript theme={null}
import { withRetry, APIError } from '@limitless-exchange/sdk';

const result = await withRetry(
  () => orderClient.createOrder({
    marketSlug: 'btc-100k-weekly',
    tokenId: market.tokens.yes,
    side: 'BUY',
    price: 0.65,
    size: 100,
    orderType: 'GTC',
  }),
  {
    statusCodes: [429, 500, 502, 503, 504],
    maxRetries: 3,
    delays: [1, 2, 4], // Seconds between retries
    onRetry: (attempt, error, delay) => {
      console.warn(`Retry ${attempt} in ${delay}s: ${error.message}`);
    },
  }
);
```

### withRetry options

| Option            | Type                                                     | Default                     | Description                                                                                                                                 |
| ----------------- | -------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `statusCodes`     | `number[]`                                               | `[429, 500, 502, 503, 504]` | HTTP status codes that trigger a retry                                                                                                      |
| `maxRetries`      | `number`                                                 | `3`                         | Maximum number of retry attempts                                                                                                            |
| `delays`          | `number[]`                                               | —                           | Explicit delays in **seconds** for each attempt. If omitted, exponential backoff is used. If fewer delays than retries, the last is reused. |
| `exponentialBase` | `number`                                                 | `2`                         | Base for exponential backoff when `delays` is omitted (`base^attempt` seconds)                                                              |
| `maxDelay`        | `number`                                                 | `60`                        | Maximum backoff delay in **seconds**                                                                                                        |
| `onRetry`         | `(attempt: number, error: Error, delay: number) => void` | —                           | Callback invoked before each retry                                                                                                          |

<Note>
  Only `APIError` instances with a matching `status` code trigger retries. Other errors (network failures, timeouts) are thrown immediately.
</Note>

## @retryOnErrors decorator

For class-based architectures, use the `@retryOnErrors` decorator to add retry logic to individual methods.

```typescript theme={null}
import { retryOnErrors } from '@limitless-exchange/sdk';

class TradingBot {
  private orderClient: OrderClient;

  constructor(orderClient: OrderClient) {
    this.orderClient = orderClient;
  }

  @retryOnErrors({
    statusCodes: [429, 500, 502, 503, 504],
    maxRetries: 5,
    exponentialBase: 2,
    maxDelay: 60,
  })
  async placeOrder(params: CreateOrderParams) {
    return this.orderClient.createOrder(params);
  }
}
```

### Decorator options

`@retryOnErrors` takes the same `RetryConfigOptions` as `withRetry`.

| Option            | Type                                                     | Default                     | Description                                                                    |
| ----------------- | -------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------ |
| `statusCodes`     | `number[]`                                               | `[429, 500, 502, 503, 504]` | HTTP status codes that trigger a retry                                         |
| `maxRetries`      | `number`                                                 | `3`                         | Maximum number of retry attempts                                               |
| `delays`          | `number[]`                                               | —                           | Explicit per-attempt delays in **seconds** (overrides exponential backoff)     |
| `exponentialBase` | `number`                                                 | `2`                         | Base for exponential backoff when `delays` is omitted (`base^attempt` seconds) |
| `maxDelay`        | `number`                                                 | `60`                        | Maximum backoff delay in **seconds**                                           |
| `onRetry`         | `(attempt: number, error: Error, delay: number) => void` | —                           | Callback invoked before each retry                                             |

When `delays` is omitted, the delay for each retry is calculated (in **seconds**) as:

```
delay = min(exponentialBase ^ attempt, maxDelay)
```

For example, with `exponentialBase: 2` and `maxDelay: 60`:

| Attempt | Delay         |
| ------- | ------------- |
| 0       | 1 s           |
| 1       | 2 s           |
| 2       | 4 s           |
| 3       | 8 s           |
| 4       | 16 s          |
| 5       | 32 s          |
| 6       | 60 s (capped) |

## Rate limiting with OrderQueue

For high-frequency trading scenarios, implement an `OrderQueue` to throttle outgoing requests and avoid 429 errors:

```typescript theme={null}
import { APIError } from '@limitless-exchange/sdk';

class OrderQueue {
  private queue: Array<() => Promise<void>> = [];
  private processing = false;
  private minDelayMs: number;

  constructor(minDelayMs = 200) {
    this.minDelayMs = minDelayMs;
  }

  async enqueue<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          resolve(await fn());
        } catch (error) {
          reject(error);
        }
      });
      this.processQueue();
    });
  }

  private async processQueue() {
    if (this.processing) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const task = this.queue.shift()!;
      await task();
      await new Promise((r) => setTimeout(r, this.minDelayMs));
    }

    this.processing = false;
  }
}

// Usage
const orderQueue = new OrderQueue(250); // 250ms between requests

const result = await orderQueue.enqueue(() =>
  orderClient.createOrder({
    marketSlug: 'btc-100k-weekly',
    tokenId: market.tokens.yes,
    side: 'BUY',
    price: 0.65,
    size: 100,
    orderType: 'GTC',
  })
);
```

## Best practices

<Accordion title="Always handle APIError specifically">
  Catch `APIError` separately from other errors. Network failures, JSON parse errors, and timeouts are not `APIError` instances and should be handled differently.

  ```typescript theme={null}
  try {
    await orderClient.createOrder({ /* ... */ });
  } catch (error) {
    if (error instanceof APIError) {
      // Server responded with an error status
      handleApiError(error);
    } else if (error instanceof TypeError) {
      // Network error (DNS, connection refused, etc.)
      console.error('Network error:', error.message);
    } else {
      // Unexpected error
      throw error;
    }
  }
  ```
</Accordion>

<Accordion title="Do not retry 400 or 401 errors">
  Client errors (400, 401, 403) indicate a problem with your request or credentials. Retrying them wastes time and API quota. Only retry transient server errors (429, 5xx).
</Accordion>

<Accordion title="Use exponential backoff for 429 responses">
  When rate limited, the server may include a `Retry-After` header. If available, respect it. Otherwise, use exponential backoff starting at 1 second.
</Accordion>

<Accordion title="Combine retry with OrderQueue">
  For the most robust setup, use `OrderQueue` to throttle request rate *and* `withRetry` or `@retryOnErrors` to handle transient failures:

  ```typescript theme={null}
  const result = await orderQueue.enqueue(() =>
    withRetry(
      () => orderClient.createOrder({ /* ... */ }),
      { statusCodes: [429, 500, 502, 503, 504], maxRetries: 3 }
    )
  );
  ```
</Accordion>

## Logging

The SDK provides optional logging through a simple `ILogger` interface. Logging is completely opt-in with zero overhead by default.

### Quick start

```typescript theme={null}
import { HttpClient, ConsoleLogger } from '@limitless-exchange/sdk';

const logger = new ConsoleLogger('info');

const httpClient = new HttpClient({
  baseURL: 'https://api.limitless.exchange',
  hmacCredentials: {
    tokenId: process.env.LMTS_TOKEN_ID,
    secret: process.env.LMTS_TOKEN_SECRET,
  },
  logger,
});
```

### Log levels

| Level   | What's logged                                                                  |
| ------- | ------------------------------------------------------------------------------ |
| `debug` | Request headers (API key redacted), request/response bodies, WebSocket events  |
| `info`  | API requests (method + URL), successful responses, order creation/cancellation |
| `warn`  | Warnings only                                                                  |
| `error` | API errors (with status code), network errors, WebSocket connection errors     |

### Custom logger for production

Implement the `ILogger` interface to integrate with your own logging infrastructure:

```typescript theme={null}
import { ILogger } from '@limitless-exchange/sdk';

class MyLogger implements ILogger {
  debug(message: string, meta?: Record<string, any>): void { /* ... */ }
  info(message: string, meta?: Record<string, any>): void { /* ... */ }
  warn(message: string, meta?: Record<string, any>): void { /* ... */ }
  error(message: string, error?: Error, meta?: Record<string, any>): void { /* ... */ }
}

const httpClient = new HttpClient({
  baseURL: 'https://api.limitless.exchange',
  hmacCredentials: {
    tokenId: process.env.LMTS_TOKEN_ID,
    secret: process.env.LMTS_TOKEN_SECRET,
  },
  logger: new MyLogger(),
});
```

### Passing logger to SDK components

All SDK components accept an optional logger:

```typescript theme={null}
const logger = new ConsoleLogger('info');

const httpClient = new HttpClient({ baseURL: '...', logger });
const marketFetcher = new MarketFetcher(httpClient, logger);
const orderClient = new OrderClient({ httpClient, wallet, logger });
```

<Note>
  The SDK automatically redacts API keys in logs (shown as `***`). Private keys are never logged.
</Note>
