Navigation

Node.js

How to Handle HTTP Errors Properly

Properly handle HTTP errors in Node.js with detailed error information, retry logic, and appropriate error responses. Complete guide for fetch(), Axios, and custom error handling.

Table Of Contents

Problem

You need to handle HTTP errors correctly in Node.js applications, distinguish between different error types, provide meaningful error messages, and implement appropriate retry logic for failed requests.

Solution

// 1. Basic HTTP Error Handling with fetch()
async function handleFetchErrors(url, options = {}) {
  try {
    const response = await fetch(url, options);
    
    // fetch() doesn't throw for HTTP error status codes
    if (!response.ok) {
      let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
      let errorDetails = null;
      
      try {
        // Try to get error details from response body
        const contentType = response.headers.get('Content-Type');
        if (contentType?.includes('application/json')) {
          errorDetails = await response.json();
          errorMessage = errorDetails.message || errorMessage;
        } else {
          errorDetails = await response.text();
        }
      } catch (parseError) {
        console.warn('Could not parse error response:', parseError.message);
      }
      
      const error = new Error(errorMessage);
      error.status = response.status;
      error.statusText = response.statusText;
      error.details = errorDetails;
      error.headers = Object.fromEntries(response.headers.entries());
      
      throw error;
    }
    
    return response;
  } catch (error) {
    // Network errors, timeouts, etc.
    if (!error.status) {
      error.type = 'NETWORK_ERROR';
    }
    
    console.error('HTTP request failed:', error.message);
    throw error;
  }
}

// 2. Detailed Error Classification
class HTTPError extends Error {
  constructor(message, status, statusText, details = null) {
    super(message);
    this.name = 'HTTPError';
    this.status = status;
    this.statusText = statusText;
    this.details = details;
    this.timestamp = new Date().toISOString();
  }
  
  isClientError() {
    return this.status >= 400 && this.status < 500;
  }
  
  isServerError() {
    return this.status >= 500 && this.status < 600;
  }
  
  isRetryable() {
    // Retry server errors and specific client errors
    return this.isServerError() || 
           this.status === 408 || // Request Timeout
           this.status === 429;   // Too Many Requests
  }
  
  toJSON() {
    return {
      name: this.name,
      message: this.message,
      status: this.status,
      statusText: this.statusText,
      details: this.details,
      timestamp: this.timestamp
    };
  }
}

async function requestWithDetailedErrors(url, options = {}) {
  try {
    const response = await fetch(url, options);
    
    if (!response.ok) {
      let errorDetails = null;
      
      try {
        const contentType = response.headers.get('Content-Type');
        if (contentType?.includes('application/json')) {
          errorDetails = await response.json();
        } else {
          errorDetails = await response.text();
        }
      } catch (parseError) {
        errorDetails = 'Could not parse error response';
      }
      
      const message = errorDetails?.message || 
                     errorDetails?.error || 
                     `HTTP ${response.status}: ${response.statusText}`;
      
      throw new HTTPError(message, response.status, response.statusText, errorDetails);
    }
    
    return await response.json();
  } catch (error) {
    if (error instanceof HTTPError) {
      throw error;
    }
    
    // Network or other errors
    const networkError = new Error(`Network error: ${error.message}`);
    networkError.type = 'NETWORK_ERROR';
    networkError.originalError = error;
    throw networkError;
  }
}

// 3. Axios Error Handling
const axios = require('axios');

function setupAxiosErrorHandling() {
  // Response interceptor for global error handling
  axios.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response) {
        // Server responded with error status
        const httpError = new HTTPError(
          error.response.data?.message || error.message,
          error.response.status,
          error.response.statusText,
          error.response.data
        );
        
        console.error('Axios HTTP Error:', httpError.toJSON());
        return Promise.reject(httpError);
      } else if (error.request) {
        // Request made but no response received
        const networkError = new Error('No response received from server');
        networkError.type = 'NETWORK_ERROR';
        networkError.request = error.request;
        
        console.error('Axios Network Error:', networkError.message);
        return Promise.reject(networkError);
      } else {
        // Something else happened
        const setupError = new Error(`Request setup error: ${error.message}`);
        setupError.type = 'SETUP_ERROR';
        
        console.error('Axios Setup Error:', setupError.message);
        return Promise.reject(setupError);
      }
    }
  );
}

// 4. Retry Logic with Exponential Backoff
async function requestWithRetry(url, options = {}, retryOptions = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 10000,
    backoffFactor = 2,
    retryCondition = (error) => error.isRetryable && error.isRetryable()
  } = retryOptions;
  
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Attempt ${attempt}/${maxRetries}`);
      
      const result = await requestWithDetailedErrors(url, options);
      return result;
    } catch (error) {
      lastError = error;
      
      // Don't retry if not retryable or on last attempt
      if (!retryCondition(error) || attempt === maxRetries) {
        break;
      }
      
      // Calculate delay with exponential backoff
      const delay = Math.min(
        baseDelay * Math.pow(backoffFactor, attempt - 1),
        maxDelay
      );
      
      console.log(`Attempt ${attempt} failed: ${error.message}`);
      console.log(`Retrying in ${delay}ms...`);
      
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// 5. Circuit Breaker Pattern
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000, monitoringPeriod = 10000) {
    this.threshold = threshold; // Failure threshold
    this.timeout = timeout; // Circuit open timeout
    this.monitoringPeriod = monitoringPeriod;
    
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failures = 0;
    this.lastFailureTime = null;
    this.successCount = 0;
  }
  
  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime >= this.timeout) {
        this.state = 'HALF_OPEN';
        this.successCount = 0;
        console.log('Circuit breaker: HALF_OPEN');
      } else {
        throw new Error('Circuit breaker is OPEN - request blocked');
      }
    }
    
    try {
      const result = await fn();
      
      if (this.state === 'HALF_OPEN') {
        this.successCount++;
        if (this.successCount >= 3) {
          this.state = 'CLOSED';
          this.failures = 0;
          console.log('Circuit breaker: CLOSED (recovered)');
        }
      } else {
        this.failures = 0;
      }
      
      return result;
    } catch (error) {
      this.failures++;
      this.lastFailureTime = Date.now();
      
      if (this.failures >= this.threshold) {
        this.state = 'OPEN';
        console.log('Circuit breaker: OPEN (threshold reached)');
      }
      
      throw error;
    }
  }
}

// 6. Error Recovery Strategies
class ErrorRecoveryManager {
  constructor() {
    this.recoveryStrategies = new Map();
  }
  
  addStrategy(errorType, strategy) {
    this.recoveryStrategies.set(errorType, strategy);
  }
  
  async handleError(error, context = {}) {
    // Handle specific HTTP status codes
    if (error.status) {
      switch (error.status) {
        case 401:
          return this.handleUnauthorized(error, context);
        case 403:
          return this.handleForbidden(error, context);
        case 404:
          return this.handleNotFound(error, context);
        case 429:
          return this.handleRateLimit(error, context);
        case 500:
        case 502:
        case 503:
        case 504:
          return this.handleServerError(error, context);
        default:
          throw error;
      }
    }
    
    // Handle network errors
    if (error.type === 'NETWORK_ERROR') {
      return this.handleNetworkError(error, context);
    }
    
    throw error;
  }
  
  async handleUnauthorized(error, context) {
    console.log('Handling 401 Unauthorized error');
    
    if (context.tokenRefreshCallback) {
      try {
        await context.tokenRefreshCallback();
        return { retry: true };
      } catch (refreshError) {
        console.error('Token refresh failed:', refreshError.message);
        return { retry: false, redirectToLogin: true };
      }
    }
    
    throw error;
  }
  
  async handleRateLimit(error, context) {
    console.log('Handling 429 Rate Limit error');
    
    const retryAfter = error.headers?.['retry-after'];
    const delay = retryAfter ? parseInt(retryAfter) * 1000 : 5000;
    
    console.log(`Rate limited, waiting ${delay}ms before retry`);
    await new Promise(resolve => setTimeout(resolve, delay));
    
    return { retry: true };
  }
  
  async handleServerError(error, context) {
    console.log(`Handling ${error.status} Server Error`);
    
    // Use exponential backoff for server errors
    const delay = Math.min(1000 * Math.pow(2, context.attempt || 0), 30000);
    await new Promise(resolve => setTimeout(resolve, delay));
    
    return { retry: true };
  }
  
  async handleNetworkError(error, context) {
    console.log('Handling Network Error');
    
    // Check if network is available
    // In browser: navigator.onLine
    // In Node.js: implement network check
    
    const delay = 2000 * (context.attempt || 1);
    await new Promise(resolve => setTimeout(resolve, delay));
    
    return { retry: true };
  }
  
  handleNotFound(error, context) {
    console.log('Handling 404 Not Found error');
    
    if (context.fallbackUrl) {
      return { retry: true, url: context.fallbackUrl };
    }
    
    throw error;
  }
  
  handleForbidden(error, context) {
    console.log('Handling 403 Forbidden error');
    throw error; // Usually not retryable
  }
}

// 7. Comprehensive Error Handler
async function robustHttpRequest(url, options = {}, context = {}) {
  const errorRecovery = new ErrorRecoveryManager();
  const circuitBreaker = new CircuitBreaker();
  
  const maxRetries = context.maxRetries || 3;
  let attempt = 0;
  
  while (attempt < maxRetries) {
    try {
      attempt++;
      
      const result = await circuitBreaker.execute(async () => {
        return await requestWithDetailedErrors(url, options);
      });
      
      return result;
    } catch (error) {
      console.error(`Attempt ${attempt} failed:`, error.message);
      
      if (attempt === maxRetries) {
        throw error;
      }
      
      try {
        const recovery = await errorRecovery.handleError(error, {
          ...context,
          attempt
        });
        
        if (!recovery.retry) {
          throw error;
        }
        
        if (recovery.url) {
          url = recovery.url;
        }
      } catch (recoveryError) {
        throw recoveryError;
      }
    }
  }
}

// Usage Examples
async function runErrorHandlingExamples() {
  console.log('=== HTTP Error Handling Examples ===');
  
  try {
    // 1. Basic error handling
    console.log('1. Testing 404 error...');
    try {
      await handleFetchErrors('https://httpbin.org/status/404');
    } catch (error) {
      console.log('Caught 404 error:', error.message);
    }
    
    // 2. Detailed error handling
    console.log('2. Testing detailed error handling...');
    try {
      await requestWithDetailedErrors('https://httpbin.org/status/500');
    } catch (error) {
      console.log('Detailed error:', error.toJSON());
    }
    
    // 3. Retry logic
    console.log('3. Testing retry logic...');
    try {
      await requestWithRetry('https://httpbin.org/status/503', {}, {
        maxRetries: 2,
        baseDelay: 500
      });
    } catch (error) {
      console.log('Retry failed:', error.message);
    }
    
    // 4. Circuit breaker
    console.log('4. Testing circuit breaker...');
    const breaker = new CircuitBreaker(2, 5000);
    
    for (let i = 0; i < 5; i++) {
      try {
        await breaker.execute(async () => {
          throw new HTTPError('Simulated failure', 500, 'Internal Server Error');
        });
      } catch (error) {
        console.log(`Circuit breaker attempt ${i + 1}:`, error.message);
      }
    }
    
  } catch (error) {
    console.error('Example error:', error.message);
  }
}

// Run examples if this file is executed directly
if (require.main === module) {
  runErrorHandlingExamples();
}

module.exports = {
  handleFetchErrors,
  HTTPError,
  requestWithDetailedErrors,
  requestWithRetry,
  CircuitBreaker,
  ErrorRecoveryManager,
  robustHttpRequest,
  setupAxiosErrorHandling
};

Explanation

Proper HTTP error handling involves checking response.ok with fetch() since it doesn't throw for HTTP error status codes. Create custom error classes with detailed information including status codes, response bodies, and timestamps.

Implement retry logic with exponential backoff for retryable errors (5xx, 408, 429). Use circuit breakers to prevent cascading failures and error recovery strategies for specific error types like 401 (refresh tokens) or 429 (rate limiting). Always log errors with sufficient context for debugging.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Node.js