Introduction
The native fetch API in Node.js (since v18) does not have a built-in timeout option like axios or node-fetch. Without a timeout, requests to slow or unresponsive servers hang indefinitely, consuming resources and blocking application flow. The correct way to add a timeout is using AbortController, but the pattern is verbose and error-prone if not implemented correctly.
Symptoms
fetch()call hangs indefinitely on slow server- No error thrown - request just never resolves
- Application becomes unresponsive waiting for external API
- Event loop blocked by pending fetch requests
- Connection pool exhausted by hanging requests
// No timeout - hangs forever if server does not respond
const response = await fetch('https://slow-api.example.com/data');
// If slow-api.example.com takes 5 minutes, this waits 5 minutes
// No error, no timeout, just indefinite waitCommon Causes
- Native fetch API does not accept
timeoutoption - No AbortController configured for the request
- External API degradation causing slow responses
- Network issues between server and external API
- DNS resolution delays
Step-by-Step Fix
- 1.Add timeout with AbortController:
- 2.```javascript
- 3.// Create timeout wrapper function
- 4.async function fetchWithTimeout(url, options = {}) {
- 5.const { timeout = 10000, ...fetchOptions } = options;
const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout);
try { const response = await fetch(url, { ...fetchOptions, signal: controller.signal, }); return response; } finally { clearTimeout(id); } }
// Usage const response = await fetchWithTimeout('https://api.example.com/data', { timeout: 5000, // 5 second timeout method: 'POST', body: JSON.stringify({ query: 'test' }), }); ```
- 1.Handle timeout errors gracefully:
- 2.```javascript
- 3.async function fetchWithRetry(url, options = {}) {
- 4.const { timeout = 10000, retries = 3, ...fetchOptions } = options;
for (let attempt = 1; attempt <= retries; attempt++) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout);
try { const response = await fetch(url, { ...fetchOptions, signal: controller.signal, });
if (!response.ok) {
throw new Error(HTTP ${response.status}: ${response.statusText});
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log(Attempt ${attempt}: Request timed out after ${timeout}ms);
} else {
console.log(Attempt ${attempt}: ${error.message});
}
if (attempt === retries) { throw error; // All retries exhausted }
// Exponential backoff await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000) ); } finally { clearTimeout(id); } } } ```
- 1.Set timeout per-request type:
- 2.```javascript
- 3.const TIMEOUTS = {
- 4.fast: 3000, // Health checks, simple queries
- 5.normal: 10000, // Standard API calls
- 6.slow: 30000, // Bulk operations, reports
- 7.};
async function apiCall(endpoint, type = 'normal') { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), TIMEOUTS[type]);
try {
const response = await fetch(https://api.example.com${endpoint}, {
signal: controller.signal,
});
return response.json();
} finally {
clearTimeout(id);
}
}
```
- 1.Use TimeoutSignal utility (Node.js 18.3+):
- 2.```javascript
- 3.// Node.js 18.3+ has AbortSignal.timeout()
- 4.const response = await fetch('https://api.example.com/data', {
- 5.signal: AbortSignal.timeout(5000), // 5 second timeout
- 6.});
// Cleaner than AbortController + setTimeout // Throws DOMException with name 'TimeoutError' on timeout ```
- 1.Add timeout to Express proxy middleware:
- 2.```javascript
- 3.const express = require('express');
- 4.const app = express();
app.get('/proxy/*', async (req, res) => { const targetUrl = req.params[0]; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000);
try { const response = await fetch(targetUrl, { signal: controller.signal, headers: req.headers, });
const data = await response.json(); res.json(data); } catch (error) { if (error.name === 'AbortError') { res.status(504).json({ error: 'Gateway timeout' }); } else { res.status(502).json({ error: 'Bad gateway' }); } } finally { clearTimeout(timeout); } }); ```
Prevention
- Always wrap fetch calls with timeout logic
- Use
AbortSignal.timeout()for Node.js 18.3+ - Set different timeouts for different types of requests
- Monitor request latency in production dashboards
- Add circuit breakers for external API dependencies
- Use
fetchwithkeepalive: truefor fire-and-forget requests - Consider using
undicifor more control over connection pooling and timeouts