Skip to content

Error Handling Guide

Table of Contents

Error Response Format

All API errors follow a consistent JSON structure for easy parsing and handling.

Standard Error Response

json
{
  "error": "ErrorType",
  "message": "Human-readable error description",
  "details": {
    "field": "Additional context about the error",
    "code": "SPECIFIC_ERROR_CODE",
    "timestamp": "2025-01-28T10:30:00Z",
    "requestId": "req-123456-abcdef"
  }
}

Field Descriptions

FieldTypeDescription
errorStringError category/type
messageStringUser-friendly error message
detailsObjectAdditional error context
details.codeStringSpecific error code for programmatic handling
details.timestampStringWhen the error occurred (ISO 8601)
details.requestIdStringUnique request identifier for support

HTTP Status Codes

Success Codes (2xx)

CodeNameDescription
200OKRequest successful
201CreatedResource created successfully
202AcceptedRequest accepted for processing
204No ContentRequest successful, no content to return

Client Error Codes (4xx)

CodeNameDescriptionCommon Causes
400Bad RequestInvalid request syntax or parametersMalformed JSON, invalid field values
401UnauthorizedAuthentication required or failedMissing/invalid tenant-code or api-version
403ForbiddenAccess denied to resourceInsufficient permissions for tenant
404Not FoundResource not foundInvalid ID or deleted resource
405Method Not AllowedHTTP method not supportedUsing GET on POST-only endpoint
409ConflictRequest conflicts with current stateDuplicate entry, concurrent modification
422Unprocessable EntityRequest understood but invalidBusiness rule violation
429Too Many RequestsRate limit exceededToo many requests in time window

Server Error Codes (5xx)

CodeNameDescriptionRetry Strategy
500Internal Server ErrorUnexpected server errorRetry with exponential backoff
502Bad GatewayInvalid upstream responseRetry after delay
503Service UnavailableService temporarily unavailableCheck status page, retry later
504Gateway TimeoutUpstream request timeoutRetry with smaller payload

Error Categories

Authentication Errors

Errors related to authentication and authorization.

json
{
  "error": "AuthenticationError",
  "message": "Invalid or missing authentication credentials",
  "details": {
    "code": "AUTH_INVALID_TENANT",
    "missing_headers": ["tenant-code"],
    "timestamp": "2025-01-28T10:30:00Z"
  }
}

Common Authentication Error Codes:

CodeDescriptionResolution
AUTH_MISSING_HEADERSRequired headers not providedInclude api-version and tenant-code headers
AUTH_INVALID_TENANTTenant code not recognizedVerify tenant code with support
AUTH_EXPIRED_TENANTTenant access expiredContact support for renewal
AUTH_IP_RESTRICTEDRequest from unauthorized IPAdd IP to whitelist

Validation Errors

Errors related to request data validation.

json
{
  "error": "ValidationError",
  "message": "Request validation failed",
  "details": {
    "code": "VALIDATION_FAILED",
    "errors": [
      {
        "field": "startDate",
        "message": "Invalid date format",
        "expected": "YYYY-MM-DD"
      },
      {
        "field": "pageSize",
        "message": "Value exceeds maximum",
        "maximum": 100
      }
    ]
  }
}

Common Validation Error Codes:

CodeDescriptionResolution
VALIDATION_REQUIRED_FIELDRequired field missingInclude all required fields
VALIDATION_INVALID_FORMATField format incorrectCheck expected format in docs
VALIDATION_OUT_OF_RANGEValue outside allowed rangeAdjust value to valid range
VALIDATION_INVALID_ENUMInvalid enumeration valueUse accepted enum values

Business Logic Errors

Errors related to business rules and constraints.

json
{
  "error": "BusinessLogicError",
  "message": "Operation violates business rules",
  "details": {
    "code": "BIZ_DUPLICATE_ENTRY",
    "entity": "BillReport",
    "conflictingId": 12345,
    "timestamp": "2025-01-28T10:30:00Z"
  }
}

Common Business Error Codes:

CodeDescriptionResolution
BIZ_DUPLICATE_ENTRYDuplicate record existsUse existing record or modify request
BIZ_INVALID_STATEInvalid state transitionCheck current state before operation
BIZ_DEPENDENCY_ERRORRequired dependency missingCreate/link required resources first
BIZ_QUOTA_EXCEEDEDResource quota exceededDelete unused resources or upgrade plan

Rate Limiting Errors

Errors when rate limits are exceeded.

json
{
  "error": "RateLimitError",
  "message": "API rate limit exceeded",
  "details": {
    "code": "RATE_LIMIT_EXCEEDED",
    "limit": 1000,
    "remaining": 0,
    "reset": 1706441400,
    "retryAfter": 60
  }
}

Response Headers:

http
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706441400
Retry-After: 60

Error Resolution

Resolution Workflow

mermaid
graph TD
    A[API Error] --> B{Error Type?}
    B -->|400| C[Fix Request Data]
    B -->|401| D[Check Authentication]
    B -->|403| E[Verify Permissions]
    B -->|429| F[Implement Backoff]
    B -->|5xx| G[Retry with Backoff]
    
    C --> H[Validate Against Schema]
    D --> I[Verify Headers]
    E --> J[Contact Support]
    F --> K[Wait and Retry]
    G --> L[Check Status Page]

Resolution Strategies by Error Code

400 Bad Request

javascript
async function handle400Error(error) {
  const { details } = error;
  
  if (details.code === 'VALIDATION_FAILED') {
    // Fix validation errors
    details.errors.forEach(err => {
      console.log(`Fix field '${err.field}': ${err.message}`);
    });
    
    // Correct the data and retry
    const correctedData = fixValidationErrors(data, details.errors);
    return retry(correctedData);
  }
  
  // Log for debugging
  console.error('Bad request:', error.message);
  return null;
}

401 Unauthorized

python
def handle_401_error(error):
    """Handle authentication errors"""
    details = error.get('details', {})
    
    if 'missing_headers' in details:
        print(f"Missing headers: {details['missing_headers']}")
        # Add missing headers and retry
        
    elif details.get('code') == 'AUTH_INVALID_TENANT':
        print("Invalid tenant code. Please verify with support.")
        # Cannot retry - need valid credentials
        raise AuthenticationError("Invalid credentials")
    
    return None

429 Rate Limited

javascript
class RateLimitHandler {
  async handleRateLimit(error, requestFn) {
    const retryAfter = error.details.retryAfter || 60;
    const resetTime = error.details.reset * 1000;
    
    console.log(`Rate limited. Retrying after ${retryAfter} seconds`);
    
    // Wait for the specified time
    await this.sleep(retryAfter * 1000);
    
    // Retry the request
    return await requestFn();
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Retry Strategies

Exponential Backoff

Recommended for handling transient errors and rate limits.

javascript
async function exponentialBackoff(fn, maxRetries = 5) {
  let lastError;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      // Don't retry on client errors (except 429)
      if (error.status >= 400 && error.status < 500 && error.status !== 429) {
        throw error;
      }
      
      // Calculate delay: 2^i * 1000ms with jitter
      const delay = Math.min(
        Math.pow(2, i) * 1000 + Math.random() * 1000,
        30000 // Max 30 seconds
      );
      
      console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// Usage
const result = await exponentialBackoff(async () => {
  return await apiClient.get('/api/1.0/bill/search');
});

Circuit Breaker Pattern

Prevent cascading failures by stopping requests when error rate is high.

python
import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = 1
    OPEN = 2
    HALF_OPEN = 3

class CircuitBreaker:
    def __init__(self, failure_threshold=5, timeout=60):
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = CircuitState.CLOSED
    
    def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.timeout:
                self.state = CircuitState.HALF_OPEN
            else:
                raise Exception("Circuit breaker is OPEN")
        
        try:
            result = func(*args, **kwargs)
            if self.state == CircuitState.HALF_OPEN:
                self.state = CircuitState.CLOSED
                self.failure_count = 0
            return result
        except Exception as e:
            self.failure_count += 1
            self.last_failure_time = time.time()
            
            if self.failure_count >= self.failure_threshold:
                self.state = CircuitState.OPEN
            
            raise e

# Usage
breaker = CircuitBreaker(failure_threshold=5, timeout=60)

try:
    result = breaker.call(api_client.get, '/api/1.0/bills')
except Exception as e:
    print(f"Request failed: {e}")

Smart Retry with Jitter

Add randomization to prevent thundering herd problem.

javascript
function smartRetry(fn, options = {}) {
  const {
    maxRetries = 3,
    initialDelay = 1000,
    maxDelay = 30000,
    factor = 2,
    jitter = true
  } = options;
  
  return new Promise((resolve, reject) => {
    let retries = 0;
    
    function attempt() {
      fn()
        .then(resolve)
        .catch(error => {
          if (retries >= maxRetries) {
            return reject(error);
          }
          
          retries++;
          
          // Calculate delay with exponential backoff
          let delay = Math.min(initialDelay * Math.pow(factor, retries - 1), maxDelay);
          
          // Add jitter to prevent thundering herd
          if (jitter) {
            delay = delay * (0.5 + Math.random());
          }
          
          console.log(`Retry ${retries}/${maxRetries} in ${delay}ms`);
          setTimeout(attempt, delay);
        });
    }
    
    attempt();
  });
}

Logging and Debugging

Structured Error Logging

javascript
class ErrorLogger {
  logError(error, context = {}) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      level: 'ERROR',
      requestId: context.requestId || this.generateRequestId(),
      tenantCode: context.tenantCode,
      endpoint: context.endpoint,
      method: context.method,
      statusCode: error.status,
      errorCode: error.details?.code,
      errorMessage: error.message,
      errorDetails: error.details,
      stackTrace: error.stack,
      userAgent: context.userAgent,
      ipAddress: context.ipAddress
    };
    
    // Send to logging service
    console.error(JSON.stringify(logEntry, null, 2));
    
    // Store for debugging
    this.storeErrorForDebug(logEntry);
    
    return logEntry.requestId;
  }
  
  generateRequestId() {
    return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
  
  storeErrorForDebug(logEntry) {
    // Store in local storage or send to error tracking service
    if (typeof window !== 'undefined' && window.localStorage) {
      const errors = JSON.parse(localStorage.getItem('api_errors') || '[]');
      errors.push(logEntry);
      // Keep only last 50 errors
      if (errors.length > 50) errors.shift();
      localStorage.setItem('api_errors', JSON.stringify(errors));
    }
  }
}

Debug Information Collection

python
import json
import logging
from datetime import datetime

class DebugCollector:
    def __init__(self):
        self.debug_info = []
    
    def collect_error_context(self, error, request, response):
        """Collect comprehensive debug information"""
        context = {
            'timestamp': datetime.utcnow().isoformat(),
            'request': {
                'method': request.method,
                'url': request.url,
                'headers': dict(request.headers),
                'body': request.body if hasattr(request, 'body') else None
            },
            'response': {
                'status': response.status_code if response else None,
                'headers': dict(response.headers) if response else None,
                'body': response.text if response else None
            },
            'error': {
                'type': type(error).__name__,
                'message': str(error),
                'details': error.__dict__ if hasattr(error, '__dict__') else None
            },
            'environment': {
                'api_version': '1.0',
                'sdk_version': '1.0.0',
                'platform': 'python'
            }
        }
        
        self.debug_info.append(context)
        return context
    
    def export_debug_log(self, filepath='debug_log.json'):
        """Export debug information for support"""
        with open(filepath, 'w') as f:
            json.dump(self.debug_info, f, indent=2, default=str)
        
        print(f"Debug log exported to {filepath}")
        return filepath

Common Error Scenarios

Scenario 1: Bulk Operation Partial Failure

javascript
async function handleBulkOperationError(operations, errors) {
  const results = {
    successful: [],
    failed: [],
    retryable: []
  };
  
  for (let i = 0; i < operations.length; i++) {
    const error = errors[i];
    
    if (!error) {
      results.successful.push(operations[i]);
    } else if (error.status >= 500 || error.status === 429) {
      results.retryable.push({
        operation: operations[i],
        error: error
      });
    } else {
      results.failed.push({
        operation: operations[i],
        error: error,
        resolution: getErrorResolution(error)
      });
    }
  }
  
  // Retry retryable operations
  if (results.retryable.length > 0) {
    console.log(`Retrying ${results.retryable.length} operations`);
    await retryOperations(results.retryable);
  }
  
  return results;
}

Scenario 2: Authentication Token Refresh

python
class TokenManager:
    def __init__(self):
        self.token = None
        self.token_expiry = None
    
    def handle_auth_error(self, error):
        """Handle authentication errors with token refresh"""
        if error.get('details', {}).get('code') == 'AUTH_TOKEN_EXPIRED':
            # Refresh token
            self.refresh_token()
            return True  # Retry request
        
        elif error.get('details', {}).get('code') == 'AUTH_INVALID_TOKEN':
            # Re-authenticate
            self.authenticate()
            return True  # Retry request
        
        return False  # Cannot handle error
    
    def refresh_token(self):
        """Refresh authentication token"""
        # Implementation depends on auth mechanism
        pass
    
    def authenticate(self):
        """Perform full authentication"""
        # Implementation depends on auth mechanism
        pass

Scenario 3: Handling Concurrent Modification

javascript
async function handleConcurrentModification(resource, updateFn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      // Get latest version
      const current = await getResource(resource.id);
      
      // Apply update
      const updated = updateFn(current);
      
      // Include version for optimistic locking
      updated.version = current.version;
      
      // Try to save
      return await saveResource(updated);
      
    } catch (error) {
      if (error.status === 409 && error.details?.code === 'CONCURRENT_MODIFICATION') {
        console.log(`Concurrent modification detected, retry ${i + 1}/${maxRetries}`);
        // Add small delay before retry
        await new Promise(resolve => setTimeout(resolve, 100 * (i + 1)));
      } else {
        throw error;
      }
    }
  }
  
  throw new Error('Failed to update resource after maximum retries');
}

Scenario 4: Graceful Degradation

python
class GracefulDegradation:
    def __init__(self):
        self.cache = {}
        self.fallback_data = {}
    
    async def get_data_with_fallback(self, endpoint, params):
        """Get data with fallback options"""
        try:
            # Try primary request
            return await self.api_client.get(endpoint, params)
            
        except Exception as error:
            # Try cache
            cache_key = f"{endpoint}:{json.dumps(params)}"
            if cache_key in self.cache:
                print("Using cached data due to error")
                return self.cache[cache_key]
            
            # Try reduced functionality
            if error.status == 503:
                print("Service unavailable, using reduced functionality")
                simplified_params = self.simplify_params(params)
                return await self.api_client.get(endpoint, simplified_params)
            
            # Use fallback data
            if endpoint in self.fallback_data:
                print("Using fallback data")
                return self.fallback_data[endpoint]
            
            # No fallback available
            raise error
    
    def simplify_params(self, params):
        """Reduce query complexity"""
        simplified = params.copy()
        # Remove optional parameters
        optional_fields = ['includeArchived', 'metadata', 'expandRelations']
        for field in optional_fields:
            simplified.pop(field, None)
        # Reduce page size
        if 'pageSize' in simplified:
            simplified['pageSize'] = min(simplified['pageSize'], 10)
        return simplified

Error Recovery Best Practices

  1. Always log errors with context for debugging
  2. Implement retry logic for transient failures
  3. Use circuit breakers to prevent cascading failures
  4. Cache successful responses for fallback
  5. Provide meaningful error messages to users
  6. Track error patterns to identify systemic issues
  7. Test error scenarios in your integration tests
  8. Document error handling in your code

For authentication setup, see AuthenticationFor API endpoints, see API Reference