Skip to main content

Overview

Proper error handling is crucial for building robust applications with Transaction API. This guide covers common error scenarios, best practices, and implementation patterns.

Error Response Format

All API errors follow a consistent format:
{
  "success": false,
  "error": {
    "status": 400,
    "code": "VALIDATION_INVALID",
    "message": "Invalid input data"
  },
  "data": null
}

Common Error Scenarios

Authentication Errors

Invalid Token

Status: 401 Code: UNAUTHORISED
{
  "success": false,
  "error": {
    "status": 401,
    "code": "UNAUTHORISED",
    "message": "Invalid or missing authentication"
  },
  "data": null
}

Invalid Credentials

Status: 401 Code: INVALID_CREDENTIALS
{
  "success": false,
  "error": {
    "status": 401,
    "code": "INVALID_CREDENTIALS",
    "message": "Invalid login credentials"
  },
  "data": null
}

Validation Errors

Invalid Input

Status: 400 Code: VALIDATION_INVALID
{
  "success": false,
  "error": {
    "status": 400,
    "code": "VALIDATION_INVALID",
    "message": "Invalid input data"
  },
  "data": null
}

Duplicate Entry

Status: 400 Code: DUPLICATE_ENTRY
{
  "success": false,
  "error": {
    "status": 400,
    "code": "DUPLICATE_ENTRY",
    "message": "Duplicate resource detected"
  },
  "data": null
}

Resource Errors

Not Found

Status: 404 Code: NOT_FOUND
{
  "success": false,
  "error": {
    "status": 404,
    "code": "NOT_FOUND",
    "message": "Resource not found"
  },
  "data": null
}

Insufficient Balance

Status: 400 Code: INSUFFICIENT_BALANCE
{
  "success": false,
  "error": {
    "status": 400,
    "code": "INSUFFICIENT_BALANCE",
    "message": "Insufficient wallet balance"
  },
  "data": null
}

Error Handling Patterns

Basic Error Handling

async function makeApiRequest(url, options) {
  try {
    const response = await fetch(url, options);
    const data = await response.json();
    
    if (!data.success) {
      throw new ApiError(data.error);
    }
    
    return data.data;
  } catch (error) {
    if (error instanceof ApiError) {
      throw error;
    }
    
    // Handle network errors
    throw new NetworkError('Network request failed', error);
  }
}

class ApiError extends Error {
  constructor(error) {
    super(error.message);
    this.status = error.status;
    this.code = error.code;
    this.message = error.message;
  }
}

class NetworkError extends Error {
  constructor(message, originalError) {
    super(message);
    this.originalError = originalError;
  }
}

Advanced Error Handling

class TransactionApiClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://api.transaction.gg';
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    const config = {
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    };
    
    try {
      const response = await fetch(url, config);
      const data = await response.json();
      
      if (!data.success) {
        return this.handleApiError(data.error, response.status);
      }
      
      return data.data;
    } catch (error) {
      return this.handleNetworkError(error);
    }
  }
  
  handleApiError(error, status) {
    switch (error.code) {
      case 'UNAUTHORISED':
        return this.handleUnauthorized();
      case 'RATE_LIMITED':
        return this.handleRateLimit();
      case 'VALIDATION_INVALID':
        return this.handleValidationError(error);
      case 'NOT_FOUND':
        return this.handleNotFound(error);
      default:
        return this.handleGenericError(error);
    }
  }
  
  handleUnauthorized() {
    // Redirect to login or refresh token
    this.redirectToLogin();
    throw new ApiError({
      status: 401,
      code: 'UNAUTHORISED',
      message: 'Please log in again'
    });
  }
  
  handleRateLimit() {
    // Implement exponential backoff
    this.scheduleRetry();
    throw new ApiError({
      status: 429,
      code: 'RATE_LIMITED',
      message: 'Rate limit exceeded. Please try again later.'
    });
  }
  
  handleValidationError(error) {
    // Show field-specific validation errors
    this.showValidationErrors(error.details);
    throw new ApiError(error);
  }
  
  handleNotFound(error) {
    // Show user-friendly not found message
    this.showNotFoundMessage();
    throw new ApiError(error);
  }
  
  handleGenericError(error) {
    // Log error and show generic message
    console.error('API Error:', error);
    this.showGenericErrorMessage();
    throw new ApiError(error);
  }
  
  handleNetworkError(error) {
    // Handle network connectivity issues
    if (!navigator.onLine) {
      this.showOfflineMessage();
    } else {
      this.showNetworkErrorMessage();
    }
    throw new NetworkError('Network request failed', error);
  }
}

Retry Logic

Exponential Backoff

class RetryHandler {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }
  
  async executeWithRetry(operation) {
    let lastError;
    
    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        
        if (!this.shouldRetry(error, attempt)) {
          throw error;
        }
        
        const delay = this.calculateDelay(attempt);
        await this.sleep(delay);
      }
    }
    
    throw lastError;
  }
  
  shouldRetry(error, attempt) {
    // Don't retry on client errors (4xx)
    if (error.status >= 400 && error.status < 500) {
      return false;
    }
    
    // Don't retry on authentication errors
    if (error.code === 'UNAUTHORISED') {
      return false;
    }
    
    // Retry on server errors (5xx) and network errors
    return error.status >= 500 || error instanceof NetworkError;
  }
  
  calculateDelay(attempt) {
    return this.baseDelay * Math.pow(2, attempt);
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Circuit Breaker Pattern

class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.threshold = threshold;
    this.timeout = timeout;
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
  }
  
  async execute(operation) {
    if (this.state === 'OPEN') {
      if (this.shouldAttemptReset()) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
    }
  }
  
  shouldAttemptReset() {
    return Date.now() - this.lastFailureTime > this.timeout;
  }
}

User-Friendly Error Messages

Error Message Mapping

const ERROR_MESSAGES = {
  'UNAUTHORISED': 'Please log in to continue',
  'INVALID_CREDENTIALS': 'Invalid email or password',
  'VALIDATION_INVALID': 'Please check your input and try again',
  'DUPLICATE_ENTRY': 'This item already exists',
  'EMAIL_ALREADY_REGISTERED': 'An account with this email already exists',
  'MERCHANT_NOT_FOUND': 'Merchant account not found',
  'PAYMENT_NOT_FOUND': 'Payment not found',
  'PRODUCT_NOT_FOUND': 'Product not found',
  'PRICE_NOT_FOUND': 'Price not found',
  'CUSTOMER_NOT_FOUND': 'Customer not found',
  'INSUFFICIENT_BALANCE': 'Insufficient balance for this operation',
  'RATE_LIMITED': 'Too many requests. Please try again later.'
};

function getUserFriendlyMessage(errorCode) {
  return ERROR_MESSAGES[errorCode] || 'An unexpected error occurred';
}

Error Display Components

// React component for displaying errors
function ErrorDisplay({ error, onRetry }) {
  if (!error) return null;
  
  const message = getUserFriendlyMessage(error.code);
  const isRetryable = error.status >= 500 || error instanceof NetworkError;
  
  return (
    <div className="error-display">
      <div className="error-icon">⚠️</div>
      <div className="error-message">{message}</div>
      {isRetryable && (
        <button onClick={onRetry} className="retry-button">
          Try Again
        </button>
      )}
    </div>
  );
}

Logging and Monitoring

Error Logging

class ErrorLogger {
  constructor() {
    this.logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'debug';
  }
  
  logError(error, context = {}) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      level: 'error',
      message: error.message,
      code: error.code,
      status: error.status,
      context,
      stack: error.stack
    };
    
    // Log to console in development
    if (this.logLevel === 'debug') {
      console.error('API Error:', logEntry);
    }
    
    // Send to monitoring service in production
    if (this.logLevel === 'error') {
      this.sendToMonitoring(logEntry);
    }
  }
  
  sendToMonitoring(logEntry) {
    // Send to your monitoring service (e.g., Sentry, LogRocket, etc.)
    // Example with Sentry:
    // Sentry.captureException(error, { extra: context });
  }
}

Error Metrics

class ErrorMetrics {
  constructor() {
    this.metrics = {
      totalErrors: 0,
      errorsByCode: {},
      errorsByStatus: {},
      errorRate: 0
    };
  }
  
  recordError(error) {
    this.metrics.totalErrors++;
    
    // Track errors by code
    if (error.code) {
      this.metrics.errorsByCode[error.code] = 
        (this.metrics.errorsByCode[error.code] || 0) + 1;
    }
    
    // Track errors by status
    if (error.status) {
      this.metrics.errorsByStatus[error.status] = 
        (this.metrics.errorsByStatus[error.status] || 0) + 1;
    }
    
    // Calculate error rate
    this.calculateErrorRate();
  }
  
  calculateErrorRate() {
    // Calculate error rate based on total requests
    // This would need to be integrated with your request tracking
  }
  
  getMetrics() {
    return this.metrics;
  }
}

Testing Error Scenarios

Unit Tests

describe('Error Handling', () => {
  test('handles authentication errors', async () => {
    const client = new TransactionApiClient('invalid_key');
    
    await expect(client.request('/auth/me'))
      .rejects.toThrow('UNAUTHORISED');
  });
  
  test('handles validation errors', async () => {
    const client = new TransactionApiClient('valid_key');
    
    await expect(client.request('/payment/create', {
      method: 'POST',
      body: JSON.stringify({ amount: 'invalid' })
    })).rejects.toThrow('VALIDATION_INVALID');
  });
  
  test('handles network errors', async () => {
    // Mock network failure
    global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
    
    const client = new TransactionApiClient('valid_key');
    
    await expect(client.request('/payment/create'))
      .rejects.toThrow('Network request failed');
  });
});

Integration Tests

describe('Error Recovery', () => {
  test('retries on server errors', async () => {
    const retryHandler = new RetryHandler(3, 100);
    let attemptCount = 0;
    
    const operation = async () => {
      attemptCount++;
      if (attemptCount < 3) {
        throw new ApiError({ status: 500, code: 'SERVER_ERROR' });
      }
      return 'success';
    };
    
    const result = await retryHandler.executeWithRetry(operation);
    expect(result).toBe('success');
    expect(attemptCount).toBe(3);
  });
});

Best Practices

1

Handle All Error Types

Implement specific handling for different error codes and scenarios.
2

Provide User Feedback

Show clear, actionable error messages to users.
3

Implement Retry Logic

Use exponential backoff for transient errors.
4

Log and Monitor

Track errors for debugging and system health monitoring.
5

Test Error Scenarios

Write tests for common error conditions.

Common Pitfalls

Problem: Treating all errors the same waySolution: Implement specific handling for different error codes
Problem: Giving up on temporary failuresSolution: Implement retry logic with exponential backoff
Problem: Showing raw error messages to usersSolution: Map error codes to user-friendly messages
Problem: Difficult to debug production issuesSolution: Implement comprehensive error logging