|
| 1 | +# Error Handling Guide |
| 2 | + |
| 3 | +PMXT implements CCXT-style unified error handling across all exchanges (Polymarket, Kalshi, Limitless). All errors follow a consistent structure with HTTP status codes, error codes, and retry semantics. |
| 4 | + |
| 5 | +## Table of Contents |
| 6 | + |
| 7 | +- [Error Class Hierarchy](#error-class-hierarchy) |
| 8 | +- [HTTP Status Code Mappings](#http-status-code-mappings) |
| 9 | +- [Error Properties](#error-properties) |
| 10 | +- [Usage Examples](#usage-examples) |
| 11 | +- [Exchange-Specific Patterns](#exchange-specific-patterns) |
| 12 | +- [Migration Guide](#migration-guide) |
| 13 | + |
| 14 | +## Error Class Hierarchy |
| 15 | + |
| 16 | +All PMXT errors extend from `BaseError`, which provides consistent properties across all error types. |
| 17 | + |
| 18 | +### Client Errors (4xx) |
| 19 | + |
| 20 | +#### **BadRequest** (400) |
| 21 | +Generic bad request error. Base class for more specific validation errors. |
| 22 | + |
| 23 | +```typescript |
| 24 | +import { BadRequest } from 'pmxt'; |
| 25 | + |
| 26 | +throw new BadRequest('Invalid parameter', 'Polymarket'); |
| 27 | +``` |
| 28 | + |
| 29 | +#### **AuthenticationError** (401) |
| 30 | +Authentication credentials are missing or invalid. |
| 31 | + |
| 32 | +```typescript |
| 33 | +import { AuthenticationError } from 'pmxt'; |
| 34 | + |
| 35 | +throw new AuthenticationError('Invalid API key', 'Polymarket'); |
| 36 | +``` |
| 37 | + |
| 38 | +#### **PermissionDenied** (403) |
| 39 | +The authenticated user doesn't have permission for this operation. |
| 40 | + |
| 41 | +```typescript |
| 42 | +import { PermissionDenied } from 'pmxt'; |
| 43 | + |
| 44 | +throw new PermissionDenied('Insufficient permissions', 'Kalshi'); |
| 45 | +``` |
| 46 | + |
| 47 | +#### **NotFound** (404) |
| 48 | +The requested resource doesn't exist. |
| 49 | + |
| 50 | +```typescript |
| 51 | +import { NotFound, OrderNotFound, MarketNotFound } from 'pmxt'; |
| 52 | + |
| 53 | +// Generic not found |
| 54 | +throw new NotFound('Resource not found', 'Limitless'); |
| 55 | + |
| 56 | +// Specific order not found |
| 57 | +throw new OrderNotFound('order-123', 'Polymarket'); |
| 58 | + |
| 59 | +// Specific market not found |
| 60 | +throw new MarketNotFound('market-456', 'Kalshi'); |
| 61 | +``` |
| 62 | + |
| 63 | +#### **RateLimitExceeded** (429) |
| 64 | +Rate limit exceeded. This error is retryable and may include `retryAfter` seconds. |
| 65 | + |
| 66 | +```typescript |
| 67 | +import { RateLimitExceeded } from 'pmxt'; |
| 68 | + |
| 69 | +// With retry-after header |
| 70 | +throw new RateLimitExceeded('Too many requests', 60, 'Polymarket'); |
| 71 | + |
| 72 | +// Without retry-after |
| 73 | +throw new RateLimitExceeded('Rate limit exceeded', undefined, 'Kalshi'); |
| 74 | +``` |
| 75 | + |
| 76 | +#### **InvalidOrder** (400) |
| 77 | +Order parameters are invalid (price, size, tick size, etc.). |
| 78 | + |
| 79 | +```typescript |
| 80 | +import { InvalidOrder } from 'pmxt'; |
| 81 | + |
| 82 | +throw new InvalidOrder('Invalid tick size: must be 0.01', 'Polymarket'); |
| 83 | +``` |
| 84 | + |
| 85 | +#### **InsufficientFunds** (400) |
| 86 | +Insufficient funds to complete the operation. |
| 87 | + |
| 88 | +```typescript |
| 89 | +import { InsufficientFunds } from 'pmxt'; |
| 90 | + |
| 91 | +throw new InsufficientFunds('Insufficient balance: need $100, have $50', 'Kalshi'); |
| 92 | +``` |
| 93 | + |
| 94 | +#### **ValidationError** (400) |
| 95 | +Input validation failed. Includes optional `field` property. |
| 96 | + |
| 97 | +```typescript |
| 98 | +import { ValidationError } from 'pmxt'; |
| 99 | + |
| 100 | +throw new ValidationError('ID cannot be empty', 'id'); |
| 101 | +``` |
| 102 | + |
| 103 | +### Server/Network Errors (5xx) |
| 104 | + |
| 105 | +#### **NetworkError** (503) |
| 106 | +Network connectivity issues. This error is retryable. |
| 107 | + |
| 108 | +```typescript |
| 109 | +import { NetworkError } from 'pmxt'; |
| 110 | + |
| 111 | +throw new NetworkError('Connection timeout', 'Polymarket'); |
| 112 | +``` |
| 113 | + |
| 114 | +#### **ExchangeNotAvailable** (503) |
| 115 | +Exchange is down or unreachable. This error is retryable. |
| 116 | + |
| 117 | +```typescript |
| 118 | +import { ExchangeNotAvailable } from 'pmxt'; |
| 119 | + |
| 120 | +throw new ExchangeNotAvailable('Exchange is temporarily unavailable', 'Limitless'); |
| 121 | +``` |
| 122 | + |
| 123 | +## HTTP Status Code Mappings |
| 124 | + |
| 125 | +| Status Code | Error Class | Retryable | Description | |
| 126 | +|------------|-------------|-----------|-------------| |
| 127 | +| 400 | `BadRequest` | No | Malformed request or invalid parameters | |
| 128 | +| 400 | `InvalidOrder` | No | Order validation failed | |
| 129 | +| 400 | `InsufficientFunds` | No | Not enough balance | |
| 130 | +| 400 | `ValidationError` | No | Input validation failed | |
| 131 | +| 401 | `AuthenticationError` | No | Invalid or missing credentials | |
| 132 | +| 403 | `PermissionDenied` | No | Insufficient permissions | |
| 133 | +| 404 | `NotFound` | No | Resource not found | |
| 134 | +| 404 | `OrderNotFound` | No | Order not found | |
| 135 | +| 404 | `MarketNotFound` | No | Market not found | |
| 136 | +| 429 | `RateLimitExceeded` | Yes | Too many requests | |
| 137 | +| 503 | `NetworkError` | Yes | Network connectivity issues | |
| 138 | +| 503 | `ExchangeNotAvailable` | Yes | Exchange down or unreachable | |
| 139 | + |
| 140 | +## Error Properties |
| 141 | + |
| 142 | +All errors extend from `BaseError` and include these properties: |
| 143 | + |
| 144 | +```typescript |
| 145 | +interface BaseError { |
| 146 | + message: string; // Human-readable error message |
| 147 | + status: number; // HTTP status code |
| 148 | + code: string; // Machine-readable error code |
| 149 | + retryable: boolean; // Whether the operation can be retried |
| 150 | + exchange?: string; // Which exchange threw the error |
| 151 | + name: string; // Error class name |
| 152 | + stack?: string; // Stack trace |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +Additional properties for specific errors: |
| 157 | + |
| 158 | +- **RateLimitExceeded**: `retryAfter?: number` - Seconds to wait before retrying |
| 159 | +- **ValidationError**: `field?: string` - Which field failed validation |
| 160 | + |
| 161 | +## Usage Examples |
| 162 | + |
| 163 | +### Basic Error Handling |
| 164 | + |
| 165 | +```typescript |
| 166 | +import { Polymarket, AuthenticationError, InsufficientFunds } from 'pmxt'; |
| 167 | + |
| 168 | +const exchange = new Polymarket({ privateKey: '0x...' }); |
| 169 | + |
| 170 | +try { |
| 171 | + const order = await exchange.createOrder({ |
| 172 | + marketId: 'market-123', |
| 173 | + outcomeId: 'outcome-456', |
| 174 | + side: 'buy', |
| 175 | + type: 'limit', |
| 176 | + price: 0.55, |
| 177 | + amount: 10 |
| 178 | + }); |
| 179 | +} catch (error) { |
| 180 | + if (error instanceof AuthenticationError) { |
| 181 | + console.error('Authentication failed. Check your API credentials.'); |
| 182 | + } else if (error instanceof InsufficientFunds) { |
| 183 | + console.error('Not enough balance to place order.'); |
| 184 | + } else { |
| 185 | + console.error('Order failed:', error.message); |
| 186 | + } |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +### Retry Logic for Retryable Errors |
| 191 | + |
| 192 | +```typescript |
| 193 | +import { Polymarket, BaseError, RateLimitExceeded } from 'pmxt'; |
| 194 | + |
| 195 | +async function fetchMarketsWithRetry(exchange: Polymarket, maxRetries = 3) { |
| 196 | + let retries = 0; |
| 197 | + |
| 198 | + while (retries < maxRetries) { |
| 199 | + try { |
| 200 | + return await exchange.fetchMarkets(); |
| 201 | + } catch (error) { |
| 202 | + if (error instanceof BaseError && error.retryable) { |
| 203 | + retries++; |
| 204 | + |
| 205 | + // Handle rate limits with backoff |
| 206 | + if (error instanceof RateLimitExceeded && error.retryAfter) { |
| 207 | + console.log(`Rate limited. Waiting ${error.retryAfter} seconds...`); |
| 208 | + await new Promise(resolve => setTimeout(resolve, error.retryAfter! * 1000)); |
| 209 | + } else { |
| 210 | + // Exponential backoff for other retryable errors |
| 211 | + const delay = Math.pow(2, retries) * 1000; |
| 212 | + console.log(`Retrying in ${delay}ms... (attempt ${retries}/${maxRetries})`); |
| 213 | + await new Promise(resolve => setTimeout(resolve, delay)); |
| 214 | + } |
| 215 | + |
| 216 | + continue; |
| 217 | + } |
| 218 | + |
| 219 | + // Non-retryable error, throw immediately |
| 220 | + throw error; |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + throw new Error('Max retries exceeded'); |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +### Server Response Structure |
| 229 | + |
| 230 | +When using the PMXT server, errors are returned in this format: |
| 231 | + |
| 232 | +```json |
| 233 | +{ |
| 234 | + "success": false, |
| 235 | + "error": { |
| 236 | + "message": "Insufficient balance: need $100, have $50", |
| 237 | + "code": "INSUFFICIENT_FUNDS", |
| 238 | + "retryable": false, |
| 239 | + "exchange": "Polymarket" |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +For rate limit errors: |
| 245 | + |
| 246 | +```json |
| 247 | +{ |
| 248 | + "success": false, |
| 249 | + "error": { |
| 250 | + "message": "Too many requests", |
| 251 | + "code": "RATE_LIMIT_EXCEEDED", |
| 252 | + "retryable": true, |
| 253 | + "exchange": "Kalshi", |
| 254 | + "retryAfter": 60 |
| 255 | + } |
| 256 | +} |
| 257 | +``` |
| 258 | + |
| 259 | +## Exchange-Specific Patterns |
| 260 | + |
| 261 | +### Polymarket |
| 262 | + |
| 263 | +**Authentication errors** (400 response): |
| 264 | +- "API key" → `AuthenticationError` |
| 265 | +- "proxy" → `AuthenticationError` |
| 266 | +- "signature type" → `AuthenticationError` |
| 267 | + |
| 268 | +**Order validation errors**: |
| 269 | +- "tick size" → `InvalidOrder` |
| 270 | + |
| 271 | +**Error message location**: `response.data.errorMsg` |
| 272 | + |
| 273 | +### Kalshi |
| 274 | + |
| 275 | +**Balance errors**: |
| 276 | +- "balance" → `InsufficientFunds` |
| 277 | + |
| 278 | +**Error message format**: `[status] message` (e.g., "[400] invalid UUID") |
| 279 | + |
| 280 | +**Error message location**: `response.data.error.message` |
| 281 | + |
| 282 | +### Limitless |
| 283 | + |
| 284 | +Uses CLOB client (similar to Polymarket): |
| 285 | +- Same authentication patterns |
| 286 | +- Same order validation patterns |
| 287 | +- Error message location: `response.data.errorMsg` |
| 288 | + |
| 289 | +## Migration Guide |
| 290 | + |
| 291 | +### Before (v1.6.0 and earlier) |
| 292 | + |
| 293 | +```typescript |
| 294 | +import { Polymarket } from 'pmxt'; |
| 295 | + |
| 296 | +const exchange = new Polymarket({ privateKey: '0x...' }); |
| 297 | + |
| 298 | +try { |
| 299 | + const markets = await exchange.fetchMarkets(); |
| 300 | + // Empty array returned on error - can't detect failure! |
| 301 | + if (markets.length === 0) { |
| 302 | + console.log('No markets found... or was there an error?'); |
| 303 | + } |
| 304 | +} catch (error) { |
| 305 | + // Generic Error - can't distinguish error types |
| 306 | + console.error('Something went wrong:', error.message); |
| 307 | +} |
| 308 | +``` |
| 309 | + |
| 310 | +### After (v1.7.0+) |
| 311 | + |
| 312 | +```typescript |
| 313 | +import { Polymarket, NetworkError, AuthenticationError, BaseError } from 'pmxt'; |
| 314 | + |
| 315 | +const exchange = new Polymarket({ privateKey: '0x...' }); |
| 316 | + |
| 317 | +try { |
| 318 | + const markets = await exchange.fetchMarkets(); |
| 319 | + // Always returns valid array or throws error |
| 320 | + if (markets.length === 0) { |
| 321 | + console.log('No markets found'); |
| 322 | + } |
| 323 | +} catch (error) { |
| 324 | + if (error instanceof NetworkError && error.retryable) { |
| 325 | + // Implement retry logic |
| 326 | + console.log('Network error, retrying...'); |
| 327 | + } else if (error instanceof AuthenticationError) { |
| 328 | + // Re-authenticate |
| 329 | + console.error('Authentication failed. Check credentials.'); |
| 330 | + } else if (error instanceof BaseError) { |
| 331 | + // Handle other PMXT errors |
| 332 | + console.error(`${error.exchange} error [${error.code}]: ${error.message}`); |
| 333 | + } else { |
| 334 | + // Unknown error |
| 335 | + console.error('Unexpected error:', error); |
| 336 | + } |
| 337 | +} |
| 338 | +``` |
| 339 | + |
| 340 | +## Breaking Changes |
| 341 | + |
| 342 | +1. **Error types**: Methods now throw custom error classes instead of generic `Error` |
| 343 | +2. **Error messages**: Standardized error messages may differ from previous format |
| 344 | +3. **Market data methods**: Now throw errors instead of returning empty arrays on failure |
| 345 | +4. **Test assertions**: Tests must check error types and properties instead of just catching generic errors |
| 346 | + |
| 347 | +## Best Practices |
| 348 | + |
| 349 | +1. **Always catch specific error types** when you need different handling logic |
| 350 | +2. **Check the `retryable` property** before implementing retry logic |
| 351 | +3. **Use the `exchange` property** to log which exchange had the error |
| 352 | +4. **Check `error.status`** if you need to map to HTTP responses |
| 353 | +5. **Use TypeScript** to get type safety for error handling |
| 354 | + |
| 355 | +## Error Codes Reference |
| 356 | + |
| 357 | +| Code | Error Class | HTTP Status | |
| 358 | +|------|-------------|-------------| |
| 359 | +| `BAD_REQUEST` | `BadRequest` | 400 | |
| 360 | +| `AUTHENTICATION_ERROR` | `AuthenticationError` | 401 | |
| 361 | +| `PERMISSION_DENIED` | `PermissionDenied` | 403 | |
| 362 | +| `NOT_FOUND` | `NotFound` | 404 | |
| 363 | +| `ORDER_NOT_FOUND` | `OrderNotFound` | 404 | |
| 364 | +| `MARKET_NOT_FOUND` | `MarketNotFound` | 404 | |
| 365 | +| `RATE_LIMIT_EXCEEDED` | `RateLimitExceeded` | 429 | |
| 366 | +| `INVALID_ORDER` | `InvalidOrder` | 400 | |
| 367 | +| `INSUFFICIENT_FUNDS` | `InsufficientFunds` | 400 | |
| 368 | +| `VALIDATION_ERROR` | `ValidationError` | 400 | |
| 369 | +| `NETWORK_ERROR` | `NetworkError` | 503 | |
| 370 | +| `EXCHANGE_NOT_AVAILABLE` | `ExchangeNotAvailable` | 503 | |
0 commit comments