TypeError: fetch failed (Node 18+ / undici)
Node’s built-in fetch threw before it got an HTTP status. The real reason is almost always on error.cause — refused, DNS, TLS, or a connect timeout — not the outer TypeError message alone.
How it shows up
TypeError: fetch failed
at node:internal/deps/undici/undici:…
# Often with a nested cause:
[cause]: Error: connect ECONNREFUSED 127.0.0.1:3000
[cause]: Error: getaddrinfo ENOTFOUND api.example.invalid
[cause]: ConnectTimeoutError: Connect Timeout Error (UND_ERR_CONNECT_TIMEOUT)
[cause]: Error: certificate has expired
[cause]: Error: self-signed certificate in certificate chain
You never see an HTTP status from the target — TCP/TLS never finished cleanly. Logging only err.message (“fetch failed”) hides the useful bit.
Node fetch vs browser Failed to fetch
| Surface | Means |
|---|---|
| Node: TypeError: fetch failed | undici could not complete the request; dig into error.cause |
| Browser: TypeError: Failed to fetch | Opaque client failure — often CORS, mixed content, adblock, or a network error the page is not allowed to name |
| CORS errors | Browser-only policy; Node fetch does not enforce CORS |
In Node there is no CORS. If the same URL works in curl but fails in Node, compare TLS trust stores, DNS, IPv6, and whether you meant a different host/port.
Read the cause chain
try {
const res = await fetch("https://api.example.com/v1/health");
console.log(res.status);
} catch (err) {
console.error(err.message); // "fetch failed"
console.error(err.cause); // the real Error
console.error(err.cause?.code); // ECONNREFUSED, ENOTFOUND, …
console.error(err.cause?.cause); // sometimes nested further
}
Cause cheat sheet
| cause / code | Likely meaning | See also |
|---|---|---|
ECONNREFUSED | Nothing accepted the TCP connection (wrong host/port, process down, IPv6 ::1 vs 127.0.0.1) | ECONNREFUSED ::1, localhost refused |
ENOTFOUND / getaddrinfo | DNS name does not resolve | getaddrinfo ENOTFOUND, ERR_NAME_NOT_RESOLVED |
UND_ERR_CONNECT_TIMEOUT / ConnectTimeoutError | TCP connect hung until undici gave up | ETIMEDOUT, ERR_CONNECTION_TIMED_OUT |
CERT_HAS_EXPIRED / certificate has expired | Peer cert dates rejected | certificate has expired |
| self-signed / UNABLE_TO_VERIFY / SELF_SIGNED_CERT_IN_CHAIN | Chain not trusted (dev cert, corporate proxy CA) | self-signed in chain, unable to get local issuer |
ECONNRESET | Peer closed the socket mid-flight | ECONNRESET |
Diagnose with curl alongside Node
# Same URL Node is calling
curl -vI "https://api.example.com/v1/health"
# Force IPv4 if you suspect ::1 / AAAA issues
curl -4 -vI "http://localhost:3000/health"
# See what Node resolves
node -e 'require("dns").lookup("api.example.com", console.log)'
If curl gets a status and Node does not, compare proxies (HTTP_PROXY/HTTPS_PROXY), custom agents, and whether the app rewrites the URL. Paste mystery tokens from Authorization headers into JWT decoder only on copies you are allowed to inspect — never log live secrets.
Fix checklist
- Log
err.cause(andcause.code) once — stop debugging the outer “fetch failed” string alone. - Confirm host + port + scheme — typo’d env URL,
httpvshttps, container name only reachable on the Docker network. - Refused — start the upstream, fix the port, try
127.0.0.1vslocalhost(IPv6). - DNS — fix the name, search domain, or cluster DNS; watch for leftover
.local/ staging hosts. - Timeout — raise undici connect timeout only after checking firewall / security group / wrong VPC; a longer timeout does not fix a black hole.
- TLS — renew the cert, install the corporate root via
NODE_EXTRA_CA_CERTS, never leaveNODE_TLS_REJECT_UNAUTHORIZED=0in production. - Not CORS — if the failure is only in the browser, switch to the Failed to fetch / CORS guides.
Related
TypeError: Failed to fetch (browser) · CORS errors explained · ECONNREFUSED ::1 · ETIMEDOUT · net::ERR_CONNECTION_TIMED_OUT · ERR_NAME_NOT_RESOLVED · getaddrinfo ENOTFOUND · certificate has expired · self-signed certificate in certificate chain · localhost refused to connect · Anvil HTTP status · Anvil URL inspect
Questions
What does TypeError: fetch failed mean in Node?
Node’s undici fetch could not complete the HTTP request. The outer message is generic; the real reason is on error.cause (refused, DNS, TLS, timeout, reset).
How is this different from TypeError: Failed to fetch in the browser?
The browser message often hides CORS or mixed content. Node does not enforce CORS; dig into error.cause for the network/TLS code instead.
How do I see the real error?
Log err.cause and err.cause.code. You should see codes like ECONNREFUSED, ENOTFOUND, CERT_HAS_EXPIRED, or UND_ERR_CONNECT_TIMEOUT.
What causes ECONNREFUSED under fetch failed?
Nothing accepted TCP on that host:port — service down, wrong port, or localhost resolving to ::1 while the app listens only on 127.0.0.1.
What is UND_ERR_CONNECT_TIMEOUT?
undici’s connect timeout: the TCP handshake never finished in time. Check firewalls, routing, and that the host is the one you think.
Can a bad certificate cause fetch failed?
Yes. Expired, self-signed, or incomplete chains appear as nested TLS errors under TypeError: fetch failed.
Does NODE_TLS_REJECT_UNAUTHORIZED=0 fix it safely?
No. It skips trust checks and is unsafe for production. Use a real cert or NODE_EXTRA_CA_CERTS for a private CA.
Published .