fetch failed is a network-level error thrown by Node.js when an HTTP request never completes, meaning your code never received a response. This is not a TypeScript or syntax error—it's a runtime failure that stops your promise chain cold.
What "fetch failed" Means
The fetch failed error appears in Node.js (version 18+) when the global fetch API cannot establish or maintain a connection to the target server. It wraps the underlying cause (like ECONNREFUSED, ENOTFOUND, or ETIMEDOUT) and gives you a single generic message, often obscuring the real problem.
Why It Happens
The three most common causes are:
- The server is unreachable — wrong URL, DNS resolution failure, or the server is down. Node can't resolve the hostname or refuses the TCP connection.
- Network timeout — the request takes longer than the default timeout (about 300 seconds in Node's fetch) and gets aborted.
- TLS/SSL issues — self-signed certificates, mismatched hostnames, or expired certs cause the handshake to fail mid-request.
Example Code That Triggers It
Here's a minimal script that reliably produces fetch failed — it points to a port that isn't listening:
// run with: node script.ts (or tsx script.ts)
async function makeRequest() {
try {
const res = await fetch('http://localhost:9999/api/data');
const data = await res.json();
console.log(data);
} catch (err) {
console.error('Error:', err.message); // "fetch failed"
}
}
makeRequest();
Run this and you'll see Error: fetch failed — the underlying cause (connect ECONNREFUSED 127.0.0.1:9999) is hidden inside the error's cause property.
How to Fix It
The fix is to surface the real cause and handle it explicitly:
async function makeRequest() {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch('http://localhost:9999/api/data', {
signal: controller.signal,
});
clearTimeout(timeout);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
console.log(data);
} catch (err) {
// Check the cause — this is where the real info lives
console.error('Request failed:', err.cause?.message || err.message);
}
}
This works because it adds a timeout (so you don't hang forever), checks the HTTP status explicitly, and reads err.cause to see the actual network error instead of the generic wrapper.
Common Mistakes That Cause This
Mistake 1: Ignoring the cause property. Developers log err.message and see "fetch failed," then spend hours debugging the wrong layer. Always check err.cause — it contains the real error like ECONNREFUSED or ENOTFOUND.
Mistake 2: No timeout handling. Node's default fetch timeout is extremely long (minutes). If a server silently drops connections, your request hangs until Node gives up. Always add an AbortController with a reasonable timeout for production code.
When Should You Worry About This?
You should worry when fetch failed happens in production against a server that's confirmed healthy. That points to infrastructure issues — DNS misconfiguration, firewall rules blocking outbound traffic, or a load balancer that's dropping connections. A one-off fetch failed is often transient (server restarting, brief network blip); a pattern of them means something structural is broken.
Next time you see fetch failed, check err.cause first — it will tell you the actual network error in one line, saving you from guessing. If you're building robust fetch wrappers, consider a retry strategy with exponential backoff for transient failures.