Node.js runtime error

Fix 'TypeError: fetch failed' in Node.js

The short version: Node 18+ ships a native fetch (undici), and when an outbound call fails it throws a deliberately generic TypeError: fetch failed - the real reason is hidden one level down in error.cause. Log error.cause and it names the actual problem: a DNS miss (ENOTFOUND), a refused connection (ECONNREFUSED), a timeout (ETIMEDOUT / UND_ERR_CONNECT_TIMEOUT), or a TLS certificate error. Fix that underlying cause, not the wrapper.

Not your exact error? Paste it into the Deploy Error Decoder →

First: read the cause

The message you see is only the wrapper. The real error is in error.cause - print it before you do anything else:

try {
  const res = await fetch(url);
} catch (err) {
  console.error("fetch failed:", err.cause?.code, err.cause?.message);
  throw err;
}

That err.cause.code - ENOTFOUND, ECONNREFUSED, ETIMEDOUT, a UND_ERR_* code, or a certificate error - is the actual problem. Everything below is keyed to it.

What the cause usually is

ENOTFOUND - DNS

The hostname did not resolve: a typo, a missing env var, or a Docker service name used where that DNS does not exist.

ECONNREFUSED - nothing listening

The host resolved but refused: the service is down, on a different port, or you used localhost inside a container.

ETIMEDOUT / UND_ERR_CONNECT_TIMEOUT

The connection never completed: a firewall or security group dropping packets, or the target is unreachable from here.

A TLS certificate error

Self-signed, expired, or an incomplete chain - or a corporate proxy intercepting TLS. The cause code names the certificate problem.

Two more gotchas

Node version and proxies

Native fetch only exists in Node 18+, so code that used to rely on a polyfill can change behavior after an upgrade. And native fetch does not read HTTP_PROXY / HTTPS_PROXY automatically - in an environment that only reaches the internet through a proxy, every call is “fetch failed” until you wire up undici’s ProxyAgent. Also watch IPv6: if a host resolves to an AAAA record that is not routable, you can see a timeout that disappears when you force IPv4.

Once you have the cause code, fix it the same way you would for any Node network call - the cause-specific guides below walk each one.

How Infraveil helps

Outbound failures, traced to the cause

A generic “fetch failed” in production is the worst kind of error: it hides the one fact you need. On your own servers, Infraveil traces outbound dependency calls and surfaces the real cause - which host, which code, how often - so a vague wrapper becomes “the payments API has been refusing connections since the deploy,” recorded and inspectable instead of guessed at from a stack trace.

Frequently asked questions

Why is “fetch failed” so vague?

Native fetch follows the web Fetch spec, which surfaces almost every network problem as the same generic TypeError. The detail is not lost - it is moved to error.cause. Node hides it at the top level by design; you get it back by reading one property down.

How do I see the real cause?

Log error.cause, not just the message. try/catch and print err.cause and err.cause.code - that is where ENOTFOUND, ECONNREFUSED, ETIMEDOUT, or a certificate error appears. Then it is the same problem as any Node network call.

Does this happen in the browser too?

The browser throws a generic “Failed to fetch” but does not expose error.cause, and the usual reasons differ - CORS, mixed content, or an unreachable server. In Node the cause is readable and almost always DNS, connection, timeout, or TLS.

Could it be my Node version or a proxy?

Both. Native fetch only exists in Node 18+, so polyfill-era code can change after an upgrade. And native fetch ignores HTTP_PROXY/HTTPS_PROXY by default - behind a proxy every call fails until you configure undici’s ProxyAgent.