How to Fix the "fetch failed" Error in Node.js
Read the cause Node.js attaches, open the guide for it, or route fetch through a proxy in code.
Updated
TL;DR
TypeError: fetch failed means Node.js's built-in fetch() got no HTTP response because the connection, DNS lookup, TLS handshake or proxy tunnel failed, and the underlying error sits in err.cause. Fix the failure that cause names; when requests must use a proxy, route them through undici's ProxyAgent.
What fetch failed looks like in each client
The first line is always the same; the [cause] part names the real failure.
| Where | What you see |
|---|---|
| Node.js 20 and 24.11, rejection not caught (line number varies) | node:internal/deps/undici/undici:13510 ... Error.captureStackTrace(err); ... TypeError: fetch failed |
| Node.js 20 and 24, console.error(err), nothing on the port | TypeError: fetch failed ... [cause]: Error: connect ECONNREFUSED 127.0.0.1:3000 |
| Node.js 20 and 24, host name not found | TypeError: fetch failed ... [cause]: Error: getaddrinfo ENOTFOUND api.example.invalid |
| Node.js 20 and 24, server's root CA missing from Node.js's list | TypeError: fetch failed ... [cause]: Error: unable to get local issuer certificate |
| Node.js 24, proxy answered CONNECT with 407 | TypeError: fetch failed ... [cause]: DOMException [Error]: Request was cancelled. ... cause: RequestAbortedError [AbortError]: Proxy response (407) !== 200 when HTTP Tunneling |
| Chrome, fetch() in a web page (for comparison) | TypeError: Failed to fetch |
Why this happens
fetch() gives every network failure the same short message and keeps the details inside.
The Fetch Standard says a network error rejects fetch() with a TypeError, so undici, the HTTP client inside Node.js, keeps the lower-level error on that TypeError's cause property.
Diagnose your fetch failed first
The first check needs only a browser; the others read what Node.js stored with the error.
Load the URL in a browser on this computer; if it opens, the site is up and Node.js likely differs in proxy or certificate setup.
Print err.cause rather than err.message; a code there such as ECONNREFUSED or ENOTFOUND is the failure fetch wrapped.
Compare the host and port in that cause with your proxy URL; if they match, Node.js never reached the proxy itself.
Run curl -v with the same HTTPS_PROXY exported; if it prints Uses proxy env variable and succeeds, fetch is skipping that variable.
When the inner error says Request was cancelled, check err.cause.cause; Proxy response (407) !== 200 there means the proxy refused the tunnel.
Solutions ranked by effectiveness
Start with the cause; the second and third fixes apply when requests must go through a proxy.
- Most common fix
Match err.cause to its own guide
Applies when the cause carries a system or TLS code. Each code already has a full guide under Related errors, so open that page rather than changing the fetch call.
ECONNREFUSED: that address and port refused the connection; open the ECONNREFUSED guide.
ENOTFOUND or EAI_AGAIN: the host name did not resolve; open the EAI_AGAIN guide.
Certificate errors: open Unable to verify the first certificate or Self-signed certificate in chain.
UND_ERR_CONNECT_TIMEOUT: no connection within 10 seconds; open the ETIMEDOUT guide.
- Check next
Send fetch through undici's ProxyAgent
Applies when your network or job requires a proxy, because fetch on its own opens a direct connection. Import fetch and ProxyAgent together from undici@7, which needs Node.js 20.18.1 or later.
// npm install undici@7 const { fetch, ProxyAgent } = require("undici"); const proxy = "http://USERNAME:PASSWORD@proxy.geonode.io:9000"; const dispatcher = new ProxyAgent(proxy); fetch("https://example.com/", { dispatcher }) .then((res) => console.log("status", res.status)) .catch((err) => { for (let e = err; e; e = e.cause) console.log(`${e.name}: ${e.message}`); }); - For developers
Turn on Node.js's built-in proxy support
Applies when an SDK or other code you cannot edit calls global fetch. It needs Node.js 24.14+ or 25.4+ (26 has it); earlier versions throw a TypeError instead of skipping it.
const http = require("node:http"); http.setGlobalProxyFromEnv({ HTTPS_PROXY: "http://USERNAME:PASSWORD@proxy.geonode.io:9000", NO_PROXY: "localhost,127.0.0.1", }); fetch("https://example.com/") .then((res) => console.log("status", res.status)) .catch((err) => { for (let e = err; e; e = e.cause) console.log(`${e.name}: ${e.message}`); });
Stop the fetch failed error from coming back
Habits that keep the real error visible and keep every request on the route you chose.
Store the inner error's code with each failed request, so dashboards count refused ports and DNS misses apart.
Create the ProxyAgent in one shared module and import it everywhere, so no fetch call leaves without it.
Pin undici to major 7 in package.json; undici 8 declares Node.js 22.19 or later.
Send one request through the proxy at startup and exit with its cause if it fails.
In CI, aim the proxy URL at a closed local port; any request that still succeeds is skipping the proxy.
Related errors
Learn more
FAQ
Basic login that ProxyAgent sends
Geonode's username and password login is Basic Authentication; ProxyAgent builds that header from your URL.


