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.Add timeout using AbortController: Set a deadline for the fetch request.
- 2.```javascript
- 3.async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
- 4.const controller = new AbortController();
- 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.Implement retry with exponential backoff: Retry timed-out requests automatically.
- 2.```javascript
- 3.async function fetchWithRetry(url, options = {}, retries = 3) {
- 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.Set timeout on response body reading too: The request may succeed but body read may hang.
- 2.```javascript
- 3.async function fetchWithFullTimeout(url, timeoutMs = 10000) {
- 4.const controller = new AbortController();
- 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