Skip to main content

Error Anatomy

All 88Pay errors follow this structure:
{
  "status": "Success",
  "code": 200,
  "message": "Operation completed",
  "data": { /* ... */ }
}

Error Categories

4xx Client Errors
πŸ›‘ Client Errors: Your code has an issue
CodeNameWhat It MeansFix It
400Bad RequestInvalid parametersValidate input
401UnauthorizedToken expired/invalidRefresh token
403ForbiddenNo accessCheck permissions
404Not FoundResource doesn’t existVerify ID
422UnprocessableBusiness rule violationCheck logic
429Too Many RequestsRate limitedSlow down
Don’t retry these (except 401 and 429) - fix your code instead!
5xx Server Errors
πŸ”„ Server Errors: Temporary glitch - safe to retry
CodeNameWhat HappenedAction
500Internal ErrorSomething broke on our endRetry
502Bad GatewayProxy issueRetry
503Service UnavailableTemporarily downRetry
504Gateway TimeoutRequest took too longRetry
Always retry these with exponential backoff. These are temporary!

The Retry Game

Level 1: Basic Retry

// ❌ Don't do this (no backoff, infinite loop risk)
async function badRetry(fn) {
  while (true) {
    try {
      return await fn();
    } catch (error) {
      console.log('Failed, trying again...');
    }
  }
}

Level 2: Smart Retry with Backoff

// βœ… Exponential backoff: 1s β†’ 2s β†’ 4s β†’ 8s
async function smartRetry(fn, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isLast = attempt === maxRetries - 1;
      const shouldRetry = [429, 500, 502, 503, 504].includes(error.status);
      
      if (!shouldRetry || isLast) throw error;
      
      const delay = Math.pow(2, attempt) * 1000;
      console.log(`πŸ’€ Waiting ${delay}ms before retry ${attempt + 1}...`);
      await sleep(delay);
    }
  }
}

Level 3: Boss Mode (Circuit Breaker)

class CircuitBreaker {
  constructor() {
    this.failures = 0;
    this.state = '🟒'; // 🟒 CLOSED β†’ πŸ”΄ OPEN β†’ 🟑 HALF_OPEN
  }
  
  async call(fn) {
    if (this.state === 'πŸ”΄') {
      throw new Error('Circuit breaker is OPEN πŸ”΄');
    }
    
    try {
      const result = await fn();
      this.failures = 0;
      this.state = '🟒';
      return result;
    } catch (error) {
      this.failures++;
      if (this.failures >= 5) {
        this.state = 'πŸ”΄';
        setTimeout(() => this.state = '🟑', 60000); // Try again in 60s
      }
      throw error;
    }
  }
}

Error Recipes

Symptom:
    { "code": 401, "message": "Invalid or expired token" }
Cure:
    const retryWith NewToken = async (fn) => {
      try {
        return await fn();
      } catch (error) {
        if (error.status === 401) {
          await tokenManager.getToken(); // Fresh token!
          return await fn(); // Try again
        }
        throw error;
      }
    };
Symptom:
    { "code": 429, "message": "Rate limit exceeded" }
Cure:
    async function handleRateLimit(fn) {
      try {
        return await fn();
      } catch (error) {
        if (error.status === 429) {
          const retryAfter = error.headers?.['retry-after'] || 60;
          console.log(`⏳ Rate limited. Cooling down for ${retryAfter}s...`);
          
          await sleep(retryAfter * 1000);
          return await fn();
        }
        throw error;
      }
    }
Prevention:
  • Cache tokens (50s TTL)
  • Queue requests
  • Use rate limiter middleware
Deep dive into rate limits β†’
Symptom:
    { "code": 500, "message": "Internal server error" }
Cure:
    // Retry with exponential backoff
    async function resilientCall(fn, retries = 3) {
      for (let i = 0; i < retries; i++) {
        try {
          return await fn();
        } catch (error) {
          if (![500, 502, 503, 504].includes(error.status)) {
            throw error; // Not retryable
          }
          
          if (i === retries - 1) throw error; // Last attempt
          
          const backoff = Math.pow(2, i) * 1000;
          console.log(`πŸ”„ Attempt ${i + 1} failed. Retrying in ${backoff}ms...`);
          await sleep(backoff);
        }
      }
    }
    
    // Usage
    const payment = await resilientCall(
      () => createPayment(data),
      3 // Max 3 retries
    );
If it persists:
Symptom:
    {
      "code": 400,
      "errors": {
        "amount": ["Amount must be greater than 0"],
        "currency": ["Invalid currency code"]
      }
    }
Cure:
    class PaymentValidator {
      static rules = {
        amount: (v) => v > 0 || 'Amount must be positive',
        currency: (v) => /^[A-Z]{3}$/.test(v) || 'Currency must be 3 letters',
        country: (v) => /^[A-Z]{3}$/.test(v) || 'Country must be 3 letters',
        method_code: (v) => v?.length > 0 || 'Method required'
      };
      
      static validate(data) {
        const errors = {};
        
        Object.entries(this.rules).forEach(([field, rule]) => {
          const result = rule(data[field]);
          if (result !== true) {
            errors[field] = result;
          }
        });
        
        if (Object.keys(errors).length > 0) {
          throw new ValidationError('Validation failed', errors);
        }
        
        return true;
      }
    }
    
    // Usage
    try {
      PaymentValidator.validate(paymentData);
      await createPayment(paymentData);
    } catch (error) {
      if (error instanceof ValidationError) {
        console.error('❌ Validation errors:', error.errors);
      }
    }
Symptom:
    { "code": 422, "message": "Payment method not available for this country" }
Cure:
    const METHOD_MATRIX = {
      COL: ['CARDS', 'NEQUI', 'DAVIPLATA', 'CASH', 'BANK_TRANSFER', 'USDT'],
      MEX: ['CARDS', 'OXXO', 'SPEI', 'USDT'],
      BRA: ['CARDS', 'PIX', 'BOLETO', 'USDT'],
      CHL: ['CARDS', 'BANK_TRANSFER', 'USDT']
    };
    
    function validateMethod(method, country) {
      const available = METHOD_MATRIX[country];
      
      if (!available) {
        throw new Error(`❌ Country ${country} not supported`);
      }
      
      if (!available.includes(method)) {
        throw new Error(
          `❌ ${method} not available in ${country}.\n` +
          `βœ… Available: ${available.join(', ')}`
        );
      }
      
      return true;
    }
    
    // Check before creating payment
    validateMethod('NEQUI', 'COL'); // βœ… OK
    validateMethod('NEQUI', 'MEX'); // ❌ Throws error

Error Monitoring Dashboard

What to Track

    // Error rate by status code
    const errorMetrics = {
      total_requests: 1000,
      errors: {
        '400': 5,   // 0.5% - validate better
        '401': 12,  // 1.2% - token issues
        '429': 3,   // 0.3% - rate limited
        '500': 2    // 0.2% - server errors
      },
      success_rate: 97.8, // 978/1000
      avg_response_time: 342 // ms
    };
    
    // Set alerts
    if (errorMetrics.errors['500'] > 10) {
      alert('🚨 High server error rate!');
    }

Production-Ready Error Handler

Complete example with all best practices:
class ResilientAPIClient {
  constructor(config) {
    this.tokenManager = new TokenManager(config);
    this.circuitBreaker = new CircuitBreaker();
    this.rateLimiter = new RateLimiter(10, 60000);
    this.logger = new Logger();
  }
  
  async call(operation, data, options = {}) {
    const maxRetries = options.retries || 3;
    const correlationId = generateId();
    
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      try {
        // Rate limit check
        await this.rateLimiter.execute(async () => {
          
          // Circuit breaker check
          return await this.circuitBreaker.execute(async () => {
            
            // Get fresh token
            const { access_token, session_id } = await this.tokenManager.getToken();
            
            // Validate input
            this.validateInput(data);
            
            // Make API call
            const startTime = Date.now();
            const response = await fetch(operation.endpoint, {
              method: operation.method,
              headers: {
                'Authorization': `Bearer ${access_token}`,
                'x-session-id': session_id,
                'Content-Type': 'application/json',
                'X-Correlation-ID': correlationId
              },
              body: JSON.stringify(data)
            });
            
            const duration = Date.now() - startTime;
            
            // Log success
            this.logger.info('API call succeeded', {
              operation: operation.name,
              duration,
              correlationId
            });
            
            return await response.json();
          });
        });
        
      } catch (error) {
        const isLast = attempt === maxRetries - 1;
        const shouldRetry = this.isRetryable(error);
        
        // Log error
        this.logger.error('API call failed', {
          operation: operation.name,
          attempt: attempt + 1,
          error: {
            code: error.status,
            message: error.message
          },
          correlationId,
          willRetry: !isLast && shouldRetry
        });
        
        // Don't retry or last attempt
        if (!shouldRetry || isLast) {
          throw this.formatError(error, correlationId);
        }
        
        // Exponential backoff
        const delay = Math.pow(2, attempt) * 1000;
        await sleep(delay);
      }
    }
  }
  
  isRetryable(error) {
    return [401, 429, 500, 502, 503, 504].includes(error.status);
  }
  
  validateInput(data) {
    // Validation logic
    if (!data.amount || data.amount <= 0) {
      throw new ValidationError('Amount must be positive');
    }
    // ... more validation
  }
  
  formatError(error, correlationId) {
    return {
      code: error.status,
      message: error.message,
      correlationId,
      userMessage: this.getUserFriendlyMessage(error),
      timestamp: new Date().toISOString()
    };
  }
  
  getUserFriendlyMessage(error) {
    const messages = {
      400: 'Please check your payment details',
      401: 'Session expired, please try again',
      422: 'This payment method is not available',
      429: 'Too many requests, please wait a moment',
      500: 'Something went wrong, please try again'
    };
    
    return messages[error.status] || 'An error occurred';
  }
}

// Usage
const client = new ResilientAPIClient({
  apiKey: process.env.EIGHTY_EIGHT_PAY_API_KEY,
  merchantId: process.env.EIGHTY_EIGHT_PAY_MERCHANT_ID
});

try {
  const payment = await client.call(
    {
      name: 'create_payment',
      method: 'POST',
      endpoint: 'https://api-sandbox.88pay.io/api/transactions/charges'
    },
    paymentData,
    { retries: 3 }
  );
  
  console.log('βœ… Payment created:', payment.data.reference);
  
} catch (error) {
  console.error('❌ Payment failed:', error.userMessage);
  console.error('Correlation ID:', error.correlationId);
}

Testing Error Scenarios

Use these techniques to test your error handling:
// Test helper to simulate errors
class MockAPI {
  static simulateError(status) {
    const error = new Error();
    error.status = status;
    error.message = {
      400: 'Bad Request',
      401: 'Unauthorized',
      429: 'Rate Limited',
      500: 'Internal Error'
    }[status];
    throw error;
  }
}

// Test retry logic
it('should retry 500 errors', async () => {
  let attempts = 0;
  
  const flaky API = async () => {
    attempts++;
    if (attempts < 3) MockAPI.simulateError(500);
    return { success: true };
  };
  
  const result = await smartRetry(flakyAPI, 3);
  
  expect(attempts).toBe(3);
  expect(result.success).toBe(true);
});

// Test circuit breaker
it('should open circuit after 5 failures', async () => {
  const breaker = new CircuitBreaker();
  
  for (let i = 0; i < 5; i++) {
    try {
      await breaker.call(() => MockAPI.simulateError(500));
    } catch (e) {}
  }
  
  expect(breaker.state).toBe('πŸ”΄'); // OPEN
});

Go-Live Checklist

  • βœ… Exponential backoff implemented for 5xx errors
  • βœ… Token refresh logic for 401 errors
  • βœ… Rate limit handling with backoff
  • βœ… Input validation prevents 400 errors
  • βœ… Circuit breaker for cascading failures
  • βœ… Structured logging (no sensitive data)
  • βœ… Error monitoring with alerts
  • βœ… User-friendly error messages
  • βœ… Correlation IDs for debugging
  • βœ… All error scenarios tested

Quick Reference

StatusRetry?BackoffMax Attempts
400❌--
401βœ… OnceImmediate1
403❌--
404❌--
422❌--
429βœ…Exponential3-5
500βœ…Exponential3
502βœ…Exponential3
503βœ…Exponential3
504βœ…Exponential3

Next Steps