Navigation

Node.js

How to Handle Request Timeouts

Handle HTTP request timeouts in Node.js using AbortController, setTimeout, and library-specific timeout options. Prevent hanging requests and improve reliability.

Table Of Contents

Problem

You need to prevent HTTP requests from hanging indefinitely by implementing proper timeout handling, ensuring your application remains responsive when external APIs are slow or unresponsive.

Solution

// 1. Native fetch() with AbortController
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeoutMs);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error.name === 'AbortError') {
      throw new Error(`Request timeout after ${timeoutMs}ms`);
    }
    
    throw error;
  }
}

// 2. Axios with Built-in Timeout
const axios = require('axios');

async function axiosWithTimeout() {
  try {
    const response = await axios.get('https://httpbin.org/delay/3', {
      timeout: 2000 // 2 seconds timeout
    });
    
    return response.data;
  } catch (error) {
    if (error.code === 'ECONNABORTED') {
      throw new Error('Request timeout - server took too long to respond');
    }
    
    throw error;
  }
}

// 3. Multiple Timeout Strategies
class RequestManager {
  constructor(defaultTimeout = 5000) {
    this.defaultTimeout = defaultTimeout;
  }
  
  // Connection timeout + read timeout
  async fetchWithDualTimeout(url, options = {}) {
    const connectionTimeout = options.connectionTimeout || 3000;
    const readTimeout = options.readTimeout || 5000;
    
    // Connection timeout
    const connectionController = new AbortController();
    const connectionTimer = setTimeout(() => {
      connectionController.abort();
    }, connectionTimeout);
    
    try {
      const response = await fetch(url, {
        ...options,
        signal: connectionController.signal
      });
      
      clearTimeout(connectionTimer);
      
      // Read timeout for response body
      const readController = new AbortController();
      const readTimer = setTimeout(() => {
        readController.abort();
      }, readTimeout);
      
      try {
        const data = await response.json();
        clearTimeout(readTimer);
        return data;
      } catch (readError) {
        clearTimeout(readTimer);
        
        if (readError.name === 'AbortError') {
          throw new Error(`Read timeout after ${readTimeout}ms`);
        }
        
        throw readError;
      }
    } catch (connectionError) {
      clearTimeout(connectionTimer);
      
      if (connectionError.name === 'AbortError') {
        throw new Error(`Connection timeout after ${connectionTimeout}ms`);
      }
      
      throw connectionError;
    }
  }
}

// 4. Promise-based Timeout Wrapper
function withTimeout(promise, timeoutMs, errorMessage) {
  return Promise.race([
    promise,
    new Promise((_, reject) => {
      setTimeout(() => {
        reject(new Error(errorMessage || `Operation timeout after ${timeoutMs}ms`));
      }, timeoutMs);
    })
  ]);
}

// Usage with any async operation
async function useTimeoutWrapper() {
  try {
    const result = await withTimeout(
      fetch('https://httpbin.org/delay/10'),
      3000,
      'API request took too long'
    );
    
    return await result.json();
  } catch (error) {
    console.error('Timeout wrapper error:', error.message);
    throw error;
  }
}

// 5. Retry with Timeout
async function retryWithTimeout(url, options = {}, maxRetries = 3, timeoutMs = 5000) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Attempt ${attempt}/${maxRetries} with ${timeoutMs}ms timeout`);
      
      const response = await fetchWithTimeout(url, options, timeoutMs);
      return await response.json();
    } catch (error) {
      lastError = error;
      
      // Increase timeout for subsequent attempts
      if (error.message.includes('timeout')) {
        timeoutMs = Math.min(timeoutMs * 1.5, 30000); // Max 30 seconds
        console.log(`Timeout occurred, increasing to ${timeoutMs}ms for next attempt`);
      }
      
      if (attempt === maxRetries) {
        break;
      }
      
      // Wait before retry
      const delay = Math.pow(2, attempt - 1) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// 6. Express.js Server Timeout Configuration
const express = require('express');

function createServerWithTimeouts() {
  const app = express();
  
  // Global request timeout middleware
  app.use((req, res, next) => {
    // Set request timeout (30 seconds)
    req.setTimeout(30000, () => {
      const error = new Error('Request timeout');
      error.status = 408;
      next(error);
    });
    
    // Set response timeout
    res.setTimeout(30000, () => {
      if (!res.headersSent) {
        res.status(408).json({
          error: 'Response timeout',
          message: 'Server took too long to respond'
        });
      }
    });
    
    next();
  });
  
  // Route with external API call
  app.get('/api/external', async (req, res, next) => {
    try {
      const data = await fetchWithTimeout(
        'https://jsonplaceholder.typicode.com/posts/1',
        {},
        5000
      );
      
      const result = await data.json();
      res.json(result);
    } catch (error) {
      next(error);
    }
  });
  
  // Error handler for timeouts
  app.use((err, req, res, next) => {
    if (err.status === 408 || err.message.includes('timeout')) {
      return res.status(408).json({
        error: 'Request Timeout',
        message: 'The request took too long to complete',
        timestamp: new Date().toISOString()
      });
    }
    
    next(err);
  });
  
  return app;
}

// 7. HTTP Client with Configurable Timeouts
class TimeoutHttpClient {
  constructor(options = {}) {
    this.defaultTimeout = options.timeout || 5000;
    this.defaultRetries = options.retries || 3;
  }
  
  async request(url, options = {}) {
    const timeout = options.timeout || this.defaultTimeout;
    const retries = options.retries || this.defaultRetries;
    
    for (let attempt = 1; attempt <= retries; attempt++) {
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);
        
        const response = await fetch(url, {
          ...options,
          signal: controller.signal
        });
        
        clearTimeout(timeoutId);
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        return response;
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log(`Attempt ${attempt} timed out after ${timeout}ms`);
          
          if (attempt === retries) {
            throw new Error(`All ${retries} attempts timed out after ${timeout}ms each`);
          }
        } else {
          throw error;
        }
        
        // Wait before retry
        if (attempt < retries) {
          const delay = 1000 * attempt; // Linear backoff
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }
  }
  
  async get(url, options = {}) {
    const response = await this.request(url, { ...options, method: 'GET' });
    return response.json();
  }
  
  async post(url, data, options = {}) {
    const response = await this.request(url, {
      ...options,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      body: JSON.stringify(data)
    });
    return response.json();
  }
}

// 8. Database Query Timeout (Example with MongoDB)
async function queryWithTimeout(collection, query, timeoutMs = 10000) {
  const queryPromise = collection.findOne(query);
  
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Database query timeout after ${timeoutMs}ms`));
    }, timeoutMs);
  });
  
  try {
    return await Promise.race([queryPromise, timeoutPromise]);
  } catch (error) {
    // Attempt to cancel the query if possible
    console.error('Query timeout:', error.message);
    throw error;
  }
}

// Usage Examples
async function runTimeoutExamples() {
  console.log('=== Timeout Examples ===');
  
  try {
    // Basic fetch with timeout
    console.log('1. Basic fetch with timeout...');
    const response1 = await fetchWithTimeout('https://httpbin.org/delay/2', {}, 3000);
    const data1 = await response1.json();
    console.log('Success:', data1.url);
  } catch (error) {
    console.error('Error:', error.message);
  }
  
  try {
    // Axios with timeout
    console.log('2. Axios with timeout...');
    const data2 = await axiosWithTimeout();
    console.log('Success:', data2.url);
  } catch (error) {
    console.error('Error:', error.message);
  }
  
  try {
    // Retry with timeout
    console.log('3. Retry with timeout...');
    const data3 = await retryWithTimeout('https://httpbin.org/delay/1', {}, 2, 2000);
    console.log('Success:', data3.url);
  } catch (error) {
    console.error('Error:', error.message);
  }
  
  try {
    // Custom HTTP client
    console.log('4. Custom HTTP client...');
    const client = new TimeoutHttpClient({ timeout: 3000, retries: 2 });
    const data4 = await client.get('https://httpbin.org/delay/1');
    console.log('Success:', data4.url);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

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

module.exports = {
  fetchWithTimeout,
  withTimeout,
  retryWithTimeout,
  TimeoutHttpClient,
  RequestManager,
  createServerWithTimeouts
};

Explanation

Request timeouts prevent applications from hanging when external services are slow or unresponsive. Use AbortController with setTimeout() for fetch() requests, or built-in timeout options for libraries like Axios.

Implement multiple timeout strategies: connection timeouts for establishing connections and read timeouts for receiving responses. Combine timeouts with retry logic, gradually increasing timeout values for subsequent attempts. Always clean up timers and provide meaningful error messages for timeout scenarios.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Node.js