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.
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.
Keep service failures and release context together
Infraveil brings managed service health, runtime logs, and release context together on your servers, with restart and rollback controls available when appropriate. Log error.cause in your application so the outbound DNS, connection, timeout, or TLS failure can be diagnosed. Detailed outbound tracing requires instrumentation suited to your application.
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.
Client-side, no signup — they run in your browser.