Introduction

Node.js 18+ includes the native fetch API, but unlike libraries like axios, it does not have a built-in timeout option. When an external API is slow or unresponsive, fetch requests hang indefinitely, consuming resources and blocking downstream processing.

This is a common issue when integrating with third-party APIs that have unpredictable response times or intermittent availability.

Symptoms

  • fetch() call hangs indefinitely with no response or error
  • Application appears frozen waiting for external API response
  • No timeout error is thrown despite the request taking minutes

Common Causes

  • Native fetch API has no default timeout
  • External API is slow or unresponsive
  • Network issues cause TCP connection to hang without closing

Step-by-Step Fix

  1. 1.Add timeout using AbortController: Set a deadline for the fetch request.
  2. 2.```javascript
  3. 3.async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
  4. 4.const controller = new AbortController();
  5. 5.const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

try { const response = await fetch(url, { ...options, signal: controller.signal, });

if (!response.ok) { throw new Error(HTTP ${response.status}: ${response.statusText}); }

return response; } catch (err) { if (err.name === 'AbortError') { throw new Error(Request to ${url} timed out after ${timeoutMs}ms); } throw err; } finally { clearTimeout(timeoutId); } }

// Usage: const response = await fetchWithTimeout('https://api.example.com/data', {}, 5000); const data = await response.json(); ```

  1. 1.Implement retry with exponential backoff: Retry timed-out requests automatically.
  2. 2.```javascript
  3. 3.async function fetchWithRetry(url, options = {}, retries = 3) {
  4. 4.const timeoutMs = options.timeout || 10000;

for (let attempt = 1; attempt <= retries; attempt++) { try { const response = await fetchWithTimeout(url, options, timeoutMs); return response; } catch (err) { if (err.message.includes('timed out') && attempt < retries) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); console.log(Attempt ${attempt} timed out, retrying in ${delay}ms...); await new Promise(resolve => setTimeout(resolve, delay)); } else { throw err; } } } } ```

  1. 1.Set timeout on response body reading too: The request may succeed but body read may hang.
  2. 2.```javascript
  3. 3.async function fetchWithFullTimeout(url, timeoutMs = 10000) {
  4. 4.const controller = new AbortController();
  5. 5.const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

try { const response = await fetch(url, { signal: controller.signal });

// Read the body with the same timeout const text = await response.text(); return JSON.parse(text); } catch (err) { if (err.name === 'AbortError') { throw new Error(Request timed out after ${timeoutMs}ms); } throw err; } finally { clearTimeout(timeoutId); } } ```

Prevention

  • Always use AbortController with a timeout for fetch requests
  • Implement retry logic with exponential backoff for transient failures
  • Use a dedicated HTTP client library (undici, axios) for production applications
  • Monitor external API response times and set timeouts based on p99 latency