Error Handling Best Practices

Robust error handling is crucial for building reliable applications with StateSet. This guide covers best practices for handling errors, implementing retry logic, and monitoring your applications effectively.

Overview

StateSet APIs return structured error responses that include:

  • Error codes: Consistent numeric or string identifiers
  • Error messages: Human-readable descriptions
  • Error details: Additional context and debugging information
  • Request IDs: Unique identifiers for tracing issues

Error Response Structure

All StateSet API errors follow this consistent format:

{
  "error": {
    "type": "validation_error",
    "code": "INVALID_PARAMETER",
    "message": "The provided email address is invalid",
    "details": {
      "field": "customer_email",
      "provided": "invalid-email",
      "expected": "valid email format"
    },
    "request_id": "req_1H9x2C2QlDjKpM2WYw5"
  }
}

Common Error Types

1. Authentication Errors (401)

import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'app.log' }),
    new winston.transports.Console()
  ]
});

try {
  const response = await statesetClient.orders.list();
} catch (error) {
  if (error.status === 401) {
    logger.error('Authentication failed', {
      error: error.message,
      apiKey: process.env.STATESET_API_KEY?.substring(0, 12) + '...',
      requestId: error.requestId
    });
    
    // Handle token refresh or re-authentication
    await refreshApiKey();
    return retryRequest();
  }
}

2. Validation Errors (400)

const createOrderWithValidation = async (orderData) => {
  try {
    return await statesetClient.orders.create(orderData);
  } catch (error) {
    if (error.status === 400) {
      logger.warn('Order validation failed', {
        validationErrors: error.details,
        orderData: sanitizeOrderData(orderData),
        requestId: error.requestId
      });
      
      // Extract specific field errors
      const fieldErrors = error.details?.fields || {};
      throw new ValidationError('Order validation failed', fieldErrors);
    }
    throw error;
  }
};

// Custom error classes for better error handling
class ValidationError extends Error {
  constructor(message, fieldErrors) {
    super(message);
    this.name = 'ValidationError';
    this.fieldErrors = fieldErrors;
  }
}

3. Rate Limiting (429)

const retryWithBackoff = async (operation, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (error.status === 429) {
        const retryAfter = error.headers?.['retry-after'] || Math.pow(2, attempt);
        
        logger.warn('Rate limit exceeded, retrying', {
          attempt,
          retryAfter,
          requestId: error.requestId
        });
        
        if (attempt === maxRetries) {
          throw new Error(`Rate limit exceeded after ${maxRetries} attempts`);
        }
        
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      } else {
        throw error;
      }
    }
  }
};

// Usage
const orders = await retryWithBackoff(() => 
  statesetClient.orders.list({ limit: 100 })
);

4. Server Errors (500+)

const handleServerError = async (operation, context) => {
  try {
    return await operation();
  } catch (error) {
    if (error.status >= 500) {
      logger.error('Server error encountered', {
        status: error.status,
        message: error.message,
        context,
        requestId: error.requestId,
        timestamp: new Date().toISOString()
      });
      
      // Send to error tracking service
      if (process.env.SENTRY_DSN) {
        Sentry.captureException(error, {
          tags: { operation: context.operation },
          extra: { requestId: error.requestId }
        });
      }
      
      // Return user-friendly error
      throw new Error('Service temporarily unavailable. Please try again later.');
    }
    throw error;
  }
};

Comprehensive Error Handler

Create a centralized error handler for consistent error processing:

class StateSetErrorHandler {
  constructor(options = {}) {
    this.logger = options.logger || winston.createLogger({
      level: 'info',
      format: winston.format.json(),
      transports: [new winston.transports.Console()]
    });
    this.retryAttempts = options.retryAttempts || 3;
    this.enableMetrics = options.enableMetrics || false;
  }

  async handleApiCall(operation, context = {}) {
    const startTime = Date.now();
    
    try {
      const result = await this.executeWithRetry(operation, context);
      
      if (this.enableMetrics) {
        this.recordMetric('api_call_success', {
          operation: context.operation,
          duration: Date.now() - startTime
        });
      }
      
      return result;
    } catch (error) {
      const errorInfo = {
        operation: context.operation,
        error: error.message,
        status: error.status,
        requestId: error.requestId,
        duration: Date.now() - startTime,
        context
      };
      
      this.logger.error('API call failed', errorInfo);
      
      if (this.enableMetrics) {
        this.recordMetric('api_call_error', errorInfo);
      }
      
      throw this.transformError(error);
    }
  }

  async executeWithRetry(operation, context, attempt = 1) {
    try {
      return await operation();
    } catch (error) {
      if (this.shouldRetry(error, attempt)) {
        const delay = this.calculateBackoffDelay(attempt);
        
        this.logger.warn('Retrying API call', {
          operation: context.operation,
          attempt,
          delay,
          error: error.message
        });
        
        await this.sleep(delay);
        return this.executeWithRetry(operation, context, attempt + 1);
      }
      
      throw error;
    }
  }

  shouldRetry(error, attempt) {
    if (attempt >= this.retryAttempts) return false;
    
    // Retry on rate limits, server errors, and network issues
    return [429, 500, 502, 503, 504].includes(error.status) ||
           error.code === 'NETWORK_ERROR' ||
           error.code === 'TIMEOUT';
  }

  calculateBackoffDelay(attempt) {
    // Exponential backoff with jitter
    const baseDelay = Math.pow(2, attempt) * 1000;
    const jitter = Math.random() * 1000;
    return baseDelay + jitter;
  }

  transformError(error) {
    // Transform API errors into application-specific errors
    switch (error.status) {
      case 400:
        return new ValidationError(error.message, error.details);
      case 401:
        return new AuthenticationError(error.message);
      case 403:
        return new AuthorizationError(error.message);
      case 404:
        return new NotFoundError(error.message);
      case 429:
        return new RateLimitError(error.message);
      default:
        return new ApiError(error.message, error.status);
    }
  }

  recordMetric(name, data) {
    // Implement your metrics collection here
    // Examples: DataDog, Prometheus, CloudWatch
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const errorHandler = new StateSetErrorHandler({
  logger: winston.createLogger({...}),
  retryAttempts: 3,
  enableMetrics: true
});

const orders = await errorHandler.handleApiCall(
  () => statesetClient.orders.list(),
  { operation: 'list_orders' }
);

Webhook Error Handling

For webhook endpoints, implement proper error handling and retry logic:

import express from 'express';
import crypto from 'crypto';

const app = express();

app.post('/webhooks/stateset', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    // Verify webhook signature
    const signature = req.headers['x-stateset-signature'];
    if (!verifyWebhookSignature(req.body, signature)) {
      logger.warn('Invalid webhook signature', {
        signature,
        ip: req.ip,
        userAgent: req.headers['user-agent']
      });
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body);
    
    logger.info('Webhook received', {
      eventType: event.type,
      eventId: event.id,
      timestamp: event.created
    });

    // Process webhook with timeout
    await Promise.race([
      processWebhookEvent(event),
      new Promise((_, reject) => 
        setTimeout(() => reject(new Error('Webhook processing timeout')), 25000)
      )
    ]);

    res.status(200).json({ received: true });
    
  } catch (error) {
    logger.error('Webhook processing failed', {
      error: error.message,
      stack: error.stack,
      body: req.body.toString(),
      headers: req.headers
    });
    
    // Return 200 to prevent retries for permanent failures
    if (error.name === 'ValidationError') {
      return res.status(200).json({ 
        error: 'Invalid event format',
        processed: false 
      });
    }
    
    // Return 5xx for temporary failures to trigger retries
    res.status(500).json({ 
      error: 'Processing failed',
      retry: true 
    });
  }
});

const verifyWebhookSignature = (payload, signature) => {
  const expectedSignature = crypto
    .createHmac('sha256', process.env.STATESET_WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
    
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
};

Error Monitoring & Alerting

Structured Logging

const createStructuredLogger = () => {
  return winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: winston.format.combine(
      winston.format.timestamp(),
      winston.format.errors({ stack: true }),
      winston.format.json()
    ),
    defaultMeta: {
      service: 'stateset-integration',
      environment: process.env.NODE_ENV,
      version: process.env.APP_VERSION
    },
    transports: [
      new winston.transports.File({ 
        filename: 'error.log', 
        level: 'error' 
      }),
      new winston.transports.File({ 
        filename: 'combined.log' 
      }),
      new winston.transports.Console({
        format: winston.format.simple()
      })
    ]
  });
};

Error Tracking Integration

import * as Sentry from '@sentry/node';

// Initialize Sentry
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  integrations: [
    new Sentry.Integrations.Http({ tracing: true }),
    new Sentry.Integrations.Express({ app }),
  ],
  tracesSampleRate: 0.1,
});

const reportError = (error, context = {}) => {
  logger.error('Error reported to Sentry', {
    error: error.message,
    context,
    errorId: Sentry.captureException(error, {
      tags: {
        component: 'stateset-integration',
        operation: context.operation
      },
      extra: context
    })
  });
};

Testing Error Scenarios

Create comprehensive error handling tests:

import { jest } from '@jest/globals';

describe('StateSet Error Handling', () => {
  let errorHandler;
  let mockStateSetClient;

  beforeEach(() => {
    mockStateSetClient = {
      orders: {
        create: jest.fn(),
        list: jest.fn()
      }
    };
    
    errorHandler = new StateSetErrorHandler({
      logger: createTestLogger(),
      retryAttempts: 2
    });
  });

  it('should retry on rate limit errors', async () => {
    const rateLimitError = new Error('Rate limit exceeded');
    rateLimitError.status = 429;
    rateLimitError.headers = { 'retry-after': '1' };

    mockStateSetClient.orders.list
      .mockRejectedValueOnce(rateLimitError)
      .mockResolvedValueOnce({ data: [] });

    const result = await errorHandler.handleApiCall(
      () => mockStateSetClient.orders.list(),
      { operation: 'list_orders' }
    );

    expect(mockStateSetClient.orders.list).toHaveBeenCalledTimes(2);
    expect(result).toEqual({ data: [] });
  });

  it('should not retry on validation errors', async () => {
    const validationError = new Error('Invalid email');
    validationError.status = 400;

    mockStateSetClient.orders.create.mockRejectedValue(validationError);

    await expect(
      errorHandler.handleApiCall(
        () => mockStateSetClient.orders.create({}),
        { operation: 'create_order' }
      )
    ).rejects.toThrow(ValidationError);

    expect(mockStateSetClient.orders.create).toHaveBeenCalledTimes(1);
  });
});

Best Practices Summary

  1. Use Structured Logging: Always log errors with context and structured data
  2. Implement Retry Logic: Automatically retry transient failures with exponential backoff
  3. Handle Rate Limits: Respect rate limits and implement proper backoff strategies
  4. Validate Early: Validate input data before making API calls
  5. Monitor Errors: Use error tracking services to monitor and alert on issues
  6. Test Error Paths: Write comprehensive tests for error scenarios
  7. Graceful Degradation: Provide fallback options when possible
  8. User-Friendly Messages: Transform technical errors into user-friendly messages

Additional Resources