Error Handling Best Practices

Robust error handling is critical for production applications. This guide covers comprehensive strategies for handling StateSet API errors gracefully, implementing retry logic, and maintaining application reliability.

Understanding StateSet Error Responses

Error Response Structure

All StateSet API errors follow a consistent structure:
{
  "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"
}

Error Categories

Client Errors (4xx)

Errors in your request
  • Authentication failures
  • Invalid parameters
  • Rate limiting
  • Resource not found

Server Errors (5xx)

Temporary service issues
  • Internal server errors
  • Service unavailable
  • Gateway timeouts
  • Maintenance mode

Common Error Codes

Authentication Errors

Cause: Invalid or missing API key
// ❌ 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;
}

Validation Errors

// ✅ 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;
}

Rate Limiting

// ✅ 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 }
  );
}

Advanced Error Handling Patterns

Circuit Breaker Pattern

// ✅ 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
  );
}

Batch Operations Error Handling

// ✅ 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);
    }
  );
}

Production Monitoring & Alerting

Error Tracking

// ✅ 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
    };
  }
}

Testing Error Scenarios

// ✅ 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();
    });
  });
});

Error Handling Checklist

Production Readiness Checklist

Before deploying to production, ensure you have:Proper error categorization - Handle different error types appropriately
Retry logic - Implement exponential backoff for retryable errors
Circuit breakers - Prevent cascading failures in distributed systems
Fallback mechanisms - Graceful degradation when APIs are unavailable
Comprehensive logging - Log errors with sufficient context for debugging
Monitoring & alerting - Set up alerts for critical error conditions
Error testing - Test all error scenarios in your test suite
Documentation - Document error handling strategies for your team

Best Practices Summary

  1. Always use environment variables for API keys and secrets
  2. Implement proper logging instead of console.log statements
  3. Handle rate limiting with intelligent retry mechanisms
  4. Use circuit breakers for external API calls
  5. Track error metrics and set up appropriate alerts
  6. Test error scenarios comprehensively
  7. Provide meaningful fallbacks when possible
  8. Never log sensitive information in error messages

Need Help?If you encounter persistent errors or need assistance with error handling strategies, our support team is here to help. Contact us at support@stateset.com with your request ID for faster resolution.