🔧 Maintenance Mode
Status: 503
iRacing is temporarily unavailable for maintenance.
Robust error handling is crucial when working with external APIs. This guide covers all aspects of error handling with the iRacing Data Client SDK.
The SDK provides a custom IRacingError class with useful properties:
class IRacingError extends Error { status?: number; // HTTP status code statusText?: string; // HTTP status text url?: string; // The URL that was requested responseData?: unknown; // Response body from API headers?: Headers; // Response headers
// Helper properties isMaintenanceMode: boolean; // True when iRacing is under maintenance isRateLimited: boolean; // True when rate limit exceeded isUnauthorized: boolean; // True when authentication fails}🔧 Maintenance Mode
Status: 503
iRacing is temporarily unavailable for maintenance.
⏱️ Rate Limiting
Status: 429
Too many requests sent in a short time period.
🔐 Authentication
Status: 401
Invalid credentials or expired session.
🚫 Not Found
Status: 404
Requested resource doesn’t exist.
🧪 Invalid Params
Status: 400
Request parameters are structurally valid but semantically invalid for the endpoint.
import { IRacingDataClient, IRacingError } from 'iracing-data-client';
const iracing = new IRacingDataClient({ auth: { type: 'password-limited', clientId: process.env.IRACING_CLIENT_ID!, clientSecret: process.env.IRACING_CLIENT_SECRET!, username: process.env.IRACING_USERNAME!, password: process.env.IRACING_PASSWORD!, },});
try { const data = await iracing.member.info(); console.log(data);} catch (error) { if (error instanceof IRacingError) { console.error('iRacing API Error:', error.message); console.error('Status:', error.status); } else { console.error('Unexpected error:', error); }}try { const data = await iracing.member.info();} catch (error) { if (error instanceof IRacingError && error.isMaintenanceMode) { console.log('🔧 iRacing is currently under maintenance'); console.log('Please try again later');
// Show user-friendly message showMaintenanceNotification();
// Schedule retry in 5 minutes setTimeout(() => retryRequest(), 5 * 60 * 1000); }}try { const data = await iracing.member.info();} catch (error) { if (error instanceof IRacingError && error.isRateLimited) { console.log('⏱️ Rate limit exceeded');
// Extract retry-after header if available const retryAfter = error.responseData?.retryAfter || 60; console.log(`Waiting ${retryAfter} seconds before retry`);
await new Promise(r => setTimeout(r, retryAfter * 1000)); // Retry the request return await iracing.member.info(); }}try { const data = await iracing.member.info();} catch (error) { if (error instanceof IRacingError && error.isUnauthorized) { console.log('🔐 Authentication failed');
// Check if credentials are present if (!process.env.IRACING_CLIENT_ID || !process.env.IRACING_CLIENT_SECRET) { throw new Error('Missing iRacing credentials in environment'); }
// Credentials might be invalid console.error('Please check your iRacing credentials'); process.exit(1); }}async function withExponentialBackoff<T>( fn: () => Promise<T>, maxRetries = 5, baseDelay = 1000): Promise<T> { let lastError: Error;
for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error as Error;
if (error instanceof IRacingError) { // Don't retry auth errors if (error.isUnauthorized) throw error;
// Don't retry 4xx errors (except rate limits) if (error.status && error.status >= 400 && error.status < 500 && !error.isRateLimited) { throw error; } }
// Calculate delay with exponential backoff const delay = baseDelay * Math.pow(2, i); console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(r => setTimeout(r, delay)); } }
throw lastError!;}
// Usageconst data = await withExponentialBackoff( () => iracing.member.info(), 5, // max 5 retries 2000 // start with 2 second delay);class CircuitBreaker { private failures = 0; private lastFailTime = 0; private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor( private threshold = 5, private timeout = 60000 // 1 minute ) {}
async execute<T>(fn: () => Promise<T>): Promise<T> { // Check if circuit should be reset if (this.state === 'open') { if (Date.now() - this.lastFailTime > this.timeout) { this.state = 'half-open'; } else { throw new Error('Circuit breaker is open'); } }
try { const result = await fn();
// Success - reset on half-open if (this.state === 'half-open') { this.state = 'closed'; this.failures = 0; }
return result; } catch (error) { this.failures++; this.lastFailTime = Date.now();
if (this.failures >= this.threshold) { this.state = 'open'; console.error(`Circuit breaker opened after ${this.failures} failures`); }
throw error; } }}
// Usageconst breaker = new CircuitBreaker(5, 60000);
try { const data = await breaker.execute(() => iracing.member.info() );} catch (error) { if (error.message === 'Circuit breaker is open') { console.log('Too many failures - circuit breaker is open'); }}class IRacingService { private cache = new Map<string, any>();
async getMemberInfo(custId: number) { try { // Try to get fresh data const data = await this.iracing.member.get({ custIds: [custId] });
// Cache successful response this.cache.set(`member:${custId}`, { data, timestamp: Date.now() });
return data; } catch (error) { if (error instanceof IRacingError) { // Use cached data as fallback const cached = this.cache.get(`member:${custId}`);
if (cached) { console.warn('Using cached data due to API error'); return cached.data; }
// No cache available if (error.isMaintenanceMode) { return this.getDefaultMemberData(custId); } }
throw error; } }
private getDefaultMemberData(custId: number) { return { members: [{ custId, displayName: 'Unknown', irating: 0, safetyRating: 0 }] }; }}class ErrorLogger { log(error: unknown, context?: Record<string, any>) { const errorData = this.formatError(error);
console.error({ timestamp: new Date().toISOString(), ...errorData, context });
// Send to monitoring service this.sendToMonitoring(errorData); }
private formatError(error: unknown) { if (error instanceof IRacingError) { return { type: 'iracing_api_error', message: error.message, status: error.status, statusText: error.statusText, isMaintenanceMode: error.isMaintenanceMode, isRateLimited: error.isRateLimited, isUnauthorized: error.isUnauthorized, responseData: error.responseData }; }
if (error instanceof Error) { return { type: 'general_error', message: error.message, stack: error.stack }; }
return { type: 'unknown_error', error: String(error) }; }
private sendToMonitoring(errorData: any) { // Send to Sentry, DataDog, etc. }}
// Usageconst logger = new ErrorLogger();
try { const data = await iracing.member.info();} catch (error) { logger.log(error, { operation: 'getMemberInfo', userId: 123456 });}import { IRacingDataClient, IRacingError } from 'iracing-data-client';
jest.mock('iracing-data-client');
describe('Error Handling', () => { it('should handle maintenance mode', async () => { const mockError = new IRacingError( 'Service Maintenance', { status: 503, statusText: 'Service Unavailable' }, );
const mockSDK = { member: { info: jest.fn().mockRejectedValue(mockError) } };
(IRacingDataClient as jest.Mock).mockImplementation(() => mockSDK);
const iracing = new IRacingDataClient({ auth: { type: 'password-limited', clientId: 'test', clientSecret: 'test', username: 'test', password: 'test' } });
await expect(iracing.member.info()).rejects.toThrow(mockError); expect(mockError.isMaintenanceMode).toBe(true); });});✅ Always Check Error Type
Use instanceof IRacingError to handle API errors specifically
✅ Implement Retries
Add retry logic for transient failures with exponential backoff
✅ Use Fallbacks
Provide cached or default data when the API is unavailable
✅ Log Errors
Implement structured logging for debugging and monitoring
❌ Don't Ignore Errors
Never silently swallow errors - at least log them
❌ Don't Retry Everything
Some errors (401, 404) shouldn’t be retried
| Status | Error Type | Retry? | Description |
|---|---|---|---|
| 401 | Unauthorized | No | Invalid credentials or expired session |
| 400 | Bad Request | No | Invalid parameter combination (SDK preflight) |
| 403 | Forbidden | No | Insufficient permissions |
| 404 | Not Found | No | Resource doesn’t exist |
| 429 | Rate Limited | Yes | Too many requests |
| 500 | Server Error | Yes | Internal server error |
| 502 | Bad Gateway | Yes | Gateway error |
| 503 | Maintenance | Yes | Service maintenance |
| 504 | Timeout | Yes | Gateway timeout |