Skip to content

Commit 3be4821

Browse files
committed
feat(core): implement unified error handling system across all exchanges
This update introduces a standardized error handling architecture across the entire core library. Key changes: - Created a set of unified error classes (AuthenticationError, InsufficientFundsError, MarketNotFoundError, etc.) in `core/src/errors.ts`. - Implemented exchange-specific error mappers for Polymarket, Kalshi, and Limitless to translate provider-specific errors into unified PMXT errors. - Updated core exchange methods (fetchMarkets, createOrder, cancelOrder, etc.) to wrap provider calls with error mapping logic. - Added `core/docs/ERRORS.md` to document the new error system and how to handle specific error types. - Integrated error mapping into the Express server middleware for consistent API error responses. - Added comprehensive unit tests for error handling and validation logic.
1 parent 72f418c commit 3be4821

33 files changed

Lines changed: 1614 additions & 219 deletions

core/docs/ERRORS.md

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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

Comments
 (0)