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!