Skip to content

Error Handling

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);
}
}
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!;
}
// Usage
const 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;
}
}
}
// Usage
const 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.
}
}
// Usage
const 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

StatusError TypeRetry?Description
401UnauthorizedNoInvalid credentials or expired session
400Bad RequestNoInvalid parameter combination (SDK preflight)
403ForbiddenNoInsufficient permissions
404Not FoundNoResource doesn’t exist
429Rate LimitedYesToo many requests
500Server ErrorYesInternal server error
502Bad GatewayYesGateway error
503MaintenanceYesService maintenance
504TimeoutYesGateway timeout