Comprehensive guide to handling errors gracefully in StateSet API integrations
{
"error": {
"type": "invalid_request_error",
"code": "INVALID_PARAMETER",
"message": "The 'email' parameter is required",
"param": "email",
"request_id": "req_abc123",
"documentation_url": "https://docs.stateset.com/api-reference/errors#invalid-parameter"
},
"timestamp": "2024-01-15T10:30:00Z"
}
// ❌ Common mistake
const client = new StateSetClient({
apiKey: 'wrong_key_format'
});
// ✅ Proper handling
const client = new StateSetClient({
apiKey: process.env.STATESET_API_KEY
});
if (!process.env.STATESET_API_KEY) {
throw new Error('STATESET_API_KEY environment variable is required');
}
try {
const customers = await client.customers.list();
} catch (error) {
if (error.code === 'UNAUTHORIZED') {
logger.error('Authentication failed - check API key', {
requestId: error.request_id,
documentation: error.documentation_url
});
// Notify operations team
await notifyAuthenticationFailure(error);
}
throw error;
}
// ✅ Comprehensive validation error handling
async function createCustomerSafely(customerData) {
try {
const customer = await client.customers.create(customerData);
logger.info('Customer created successfully', { customerId: customer.id });
return customer;
} catch (error) {
if (error.code === 'INVALID_PARAMETER') {
const validationErrors = error.details?.validation_errors || [];
logger.warn('Customer creation failed - validation errors', {
errors: validationErrors,
requestId: error.request_id,
inputData: sanitizeForLogging(customerData)
});
// Return user-friendly error messages
return {
success: false,
errors: validationErrors.map(err => ({
field: err.param,
message: err.message,
code: err.code
}))
};
}
if (error.code === 'DUPLICATE_RESOURCE') {
logger.info('Customer already exists', {
email: customerData.email,
existingId: error.details?.existing_resource_id
});
// Return existing customer
return await client.customers.get(error.details.existing_resource_id);
}
throw error; // Re-throw unexpected errors
}
}
// Helper function to sanitize sensitive data
function sanitizeForLogging(data) {
const sanitized = { ...data };
if (sanitized.email) sanitized.email = maskEmail(sanitized.email);
if (sanitized.phone) sanitized.phone = maskPhone(sanitized.phone);
return sanitized;
}
// ✅ Intelligent rate limit handling with exponential backoff
class RateLimitHandler {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 3;
this.baseDelay = options.baseDelay || 1000;
this.maxDelay = options.maxDelay || 30000;
this.jitterFactor = options.jitterFactor || 0.1;
}
async executeWithRetry(operation, context = {}) {
let lastError;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (error.code !== 'RATE_LIMIT_EXCEEDED') {
throw error; // Not a rate limit error
}
if (attempt === this.maxRetries) {
logger.error('Max retries exceeded for rate limited request', {
attempts: attempt + 1,
context,
requestId: error.request_id
});
throw error;
}
const delay = this.calculateDelay(attempt, error);
logger.warn('Rate limited - retrying request', {
attempt: attempt + 1,
delayMs: delay,
retryAfter: error.headers?.['retry-after'],
context,
requestId: error.request_id
});
await this.sleep(delay);
}
}
throw lastError;
}
calculateDelay(attempt, error) {
// Use server-provided retry-after if available
const retryAfter = error.headers?.['retry-after'];
if (retryAfter) {
return Math.min(parseInt(retryAfter) * 1000, this.maxDelay);
}
// Exponential backoff with jitter
const exponentialDelay = this.baseDelay * Math.pow(2, attempt);
const jitter = exponentialDelay * this.jitterFactor * Math.random();
return Math.min(exponentialDelay + jitter, this.maxDelay);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage example
const rateLimitHandler = new RateLimitHandler({
maxRetries: 5,
baseDelay: 1000,
maxDelay: 60000
});
async function createCustomerWithRetry(customerData) {
return await rateLimitHandler.executeWithRetry(
() => client.customers.create(customerData),
{ operation: 'customer.create', email: customerData.email }
);
}
// ✅ Circuit breaker to prevent cascading failures
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.timeout = options.timeout || 60000;
this.monitoringPeriod = options.monitoringPeriod || 10000;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.lastFailureTime = null;
this.nextAttempt = null;
}
async execute(operation, fallback = null) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
logger.warn('Circuit breaker is OPEN - using fallback', {
nextAttempt: new Date(this.nextAttempt).toISOString(),
failureCount: this.failureCount
});
if (fallback) return await fallback();
throw new Error('Circuit breaker is OPEN - service unavailable');
}
this.state = 'HALF_OPEN';
logger.info('Circuit breaker entering HALF_OPEN state');
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
logger.info('Circuit breaker reset to CLOSED state');
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
logger.error('Circuit breaker opened due to failures', {
failureCount: this.failureCount,
nextAttempt: new Date(this.nextAttempt).toISOString()
});
}
}
}
// Usage with StateSet API
const circuitBreaker = new CircuitBreaker({
failureThreshold: 3,
timeout: 30000
});
async function getCustomerWithFallback(customerId) {
return await circuitBreaker.execute(
() => client.customers.get(customerId),
() => getCachedCustomer(customerId) // Fallback to cache
);
}
// ✅ Robust batch processing with partial failure handling
class BatchProcessor {
constructor(options = {}) {
this.batchSize = options.batchSize || 100;
this.maxConcurrency = options.maxConcurrency || 5;
this.retryFailures = options.retryFailures !== false;
}
async processBatch(items, processor) {
const batches = this.createBatches(items);
const results = [];
const failures = [];
for (const batch of batches) {
try {
const batchResults = await this.processBatchConcurrently(batch, processor);
results.push(...batchResults.successes);
failures.push(...batchResults.failures);
} catch (error) {
logger.error('Batch processing failed completely', {
batchSize: batch.length,
error: error.message
});
// Add all items in failed batch to failures
failures.push(...batch.map(item => ({
item,
error: error.message,
retryable: this.isRetryableError(error)
})));
}
}
// Retry failures if enabled
if (this.retryFailures && failures.length > 0) {
const retryableFailures = failures.filter(f => f.retryable);
if (retryableFailures.length > 0) {
logger.info('Retrying failed batch items', {
retryCount: retryableFailures.length,
totalFailures: failures.length
});
const retryResults = await this.retryFailures(retryableFailures, processor);
results.push(...retryResults.successes);
// Update failures list with remaining failures
const remainingFailures = failures.filter(f => !f.retryable);
remainingFailures.push(...retryResults.failures);
}
}
return {
successes: results,
failures,
summary: {
total: items.length,
succeeded: results.length,
failed: failures.length,
successRate: (results.length / items.length) * 100
}
};
}
createBatches(items) {
const batches = [];
for (let i = 0; i < items.length; i += this.batchSize) {
batches.push(items.slice(i, i + this.batchSize));
}
return batches;
}
async processBatchConcurrently(batch, processor) {
const semaphore = new Semaphore(this.maxConcurrency);
const successes = [];
const failures = [];
const promises = batch.map(async (item, index) => {
await semaphore.acquire();
try {
const result = await processor(item, index);
successes.push({ item, result });
} catch (error) {
failures.push({
item,
error: error.message,
code: error.code,
retryable: this.isRetryableError(error)
});
} finally {
semaphore.release();
}
});
await Promise.all(promises);
return { successes, failures };
}
isRetryableError(error) {
const retryableCodes = [
'RATE_LIMIT_EXCEEDED',
'INTERNAL_SERVER_ERROR',
'SERVICE_UNAVAILABLE',
'GATEWAY_TIMEOUT'
];
return retryableCodes.includes(error.code);
}
}
// Simple semaphore implementation
class Semaphore {
constructor(limit) {
this.limit = limit;
this.current = 0;
this.queue = [];
}
async acquire() {
return new Promise((resolve) => {
if (this.current < this.limit) {
this.current++;
resolve();
} else {
this.queue.push(resolve);
}
});
}
release() {
this.current--;
if (this.queue.length > 0) {
const next = this.queue.shift();
this.current++;
next();
}
}
}
// Usage example
const batchProcessor = new BatchProcessor({
batchSize: 50,
maxConcurrency: 3
});
async function createCustomersBatch(customerDataList) {
return await batchProcessor.processBatch(
customerDataList,
async (customerData) => {
return await client.customers.create(customerData);
}
);
}
// ✅ Comprehensive error tracking and metrics
class ErrorTracker {
constructor() {
this.errorCounts = new Map();
this.errorRates = new Map();
this.alertThresholds = {
errorRate: 0.05, // 5% error rate
consecutiveErrors: 10,
criticalErrors: ['UNAUTHORIZED', 'FORBIDDEN']
};
}
trackError(error, context = {}) {
const errorKey = `${error.code}_${context.operation || 'unknown'}`;
// Update error counts
this.errorCounts.set(errorKey, (this.errorCounts.get(errorKey) || 0) + 1);
// Log error with context
logger.error('StateSet API error tracked', {
code: error.code,
message: error.message,
operation: context.operation,
requestId: error.request_id,
timestamp: new Date().toISOString(),
...context
});
// Check for critical errors
if (this.alertThresholds.criticalErrors.includes(error.code)) {
this.sendCriticalAlert(error, context);
}
// Check error rate thresholds
this.checkErrorRateThreshold(errorKey, context);
}
trackSuccess(context = {}) {
const operationKey = context.operation || 'unknown';
this.updateSuccessRate(operationKey);
}
sendCriticalAlert(error, context) {
// Integration with alerting system (PagerDuty, Slack, etc.)
const alert = {
severity: 'critical',
title: `StateSet API Critical Error: ${error.code}`,
description: error.message,
context,
timestamp: new Date().toISOString(),
requestId: error.request_id
};
// Send alert to monitoring system
this.sendAlert(alert);
}
async sendAlert(alert) {
try {
// Example: Send to Slack webhook
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🚨 ${alert.title}`,
attachments: [{
color: alert.severity === 'critical' ? 'danger' : 'warning',
fields: [
{ title: 'Description', value: alert.description, short: false },
{ title: 'Request ID', value: alert.requestId, short: true },
{ title: 'Context', value: JSON.stringify(alert.context), short: true }
]
}]
})
});
} catch (alertError) {
logger.error('Failed to send alert', {
originalAlert: alert,
alertError: alertError.message
});
}
}
}
// Global error tracker instance
const errorTracker = new ErrorTracker();
// Enhanced API client with error tracking
class TrackedStateSetClient {
constructor(options) {
this.client = new StateSetClient(options);
this.errorTracker = errorTracker;
}
async makeRequest(operation, requestFn) {
try {
const result = await requestFn();
this.errorTracker.trackSuccess({ operation });
return result;
} catch (error) {
this.errorTracker.trackError(error, { operation });
throw error;
}
}
// Wrap all client methods
get customers() {
return {
create: (data) => this.makeRequest('customers.create', () =>
this.client.customers.create(data)
),
get: (id) => this.makeRequest('customers.get', () =>
this.client.customers.get(id)
),
list: (params) => this.makeRequest('customers.list', () =>
this.client.customers.list(params)
)
// ... other methods
};
}
}
// ✅ Comprehensive error scenario testing
describe('StateSet API Error Handling', () => {
let client;
beforeEach(() => {
client = new TrackedStateSetClient({
apiKey: process.env.STATESET_TEST_API_KEY
});
});
describe('Authentication Errors', () => {
test('handles invalid API key gracefully', async () => {
const invalidClient = new StateSetClient({ apiKey: 'invalid_key' });
await expect(invalidClient.customers.list()).rejects.toMatchObject({
code: 'UNAUTHORIZED',
message: expect.stringContaining('Invalid API key')
});
});
test('handles missing API key', async () => {
expect(() => new StateSetClient({})).toThrow('API key is required');
});
});
describe('Rate Limiting', () => {
test('handles rate limit with retry', async () => {
const rateLimitHandler = new RateLimitHandler({ maxRetries: 2 });
// Mock rate limited responses
const mockOperation = jest.fn()
.mockRejectedValueOnce({ code: 'RATE_LIMIT_EXCEEDED', headers: { 'retry-after': '1' } })
.mockResolvedValueOnce({ id: 'cust_123' });
const result = await rateLimitHandler.executeWithRetry(mockOperation);
expect(mockOperation).toHaveBeenCalledTimes(2);
expect(result).toEqual({ id: 'cust_123' });
});
});
describe('Circuit Breaker', () => {
test('opens circuit after threshold failures', async () => {
const circuitBreaker = new CircuitBreaker({ failureThreshold: 2, timeout: 1000 });
const failingOperation = jest.fn().mockRejectedValue(new Error('Service error'));
const fallback = jest.fn().mockResolvedValue({ cached: true });
// Trigger circuit breaker
await expect(circuitBreaker.execute(failingOperation)).rejects.toThrow();
await expect(circuitBreaker.execute(failingOperation)).rejects.toThrow();
// Circuit should now be open
const result = await circuitBreaker.execute(failingOperation, fallback);
expect(result).toEqual({ cached: true });
expect(fallback).toHaveBeenCalled();
});
});
});