TypeError: Failed to fetch (tested)

TypeError: Failed to fetch is what Chrome's fetch() throws when no usable response came back. We reproduced 15 different causes in Chromium 153. Every one of them produced exactly the same message. The real reason is only in the DevTools console and Network tab, never in the error object.

1. The short answer

The message tells you almost nothing. In our tests CORS errors, a server that was down, a DNS failure, a bad certificate, mixed content, an ad-blocker block, a CSP block, an offline browser and a dropped connection all threw the same thing:

TypeError: Failed to fetch

The error had no cause (it was undefined). Your JavaScript cannot see which case it was. That is on purpose: if a page could tell "CORS blocked" from "host does not exist", it could scan networks it should not see.

To find the cause, open DevTools. The Console shows a line like Failed to load resource: net::ERR_CONNECTION_REFUSED or a CORS message. The Network tab shows the same code in the Status column.

2. Our test setup

A page on http://app.test:8983 called fetch() on http://api.test:8983 (a different origin), or on HTTPS port 8984 with a self-signed certificate. Both names pointed to 127.0.0.1 through Chromium's --host-resolver-rules. For each case we logged the thrown error, the console lines and Playwright's requestfailed reason. The server logged which requests actually arrived.

try {
  await fetch('http://api.test:8983/nocors');
} catch (e) {
  console.log(e.name, e.message, e.cause); // TypeError Failed to fetch undefined
}

3. Results: cause, message, console line

Causefetch() threwConsole / Network showsReached server?
CORS: no Access-Control-Allow-OriginTypeError: Failed to fetch"has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource." then net::ERR_FAILEDYes, server sent 200
CORS preflight gets 404TypeError: Failed to fetch"Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header..." then net::ERR_FAILEDOnly the OPTIONS request
CORS preflight: method not allowedTypeError: Failed to fetch"Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response."Only OPTIONS
Server returns 500 without CORS headersTypeError: Failed to fetchSame "No 'Access-Control-Allow-Origin' header" CORS lineYes
Server down (connection refused)TypeError: Failed to fetchnet::ERR_CONNECTION_REFUSEDNo
DNS name does not existTypeError: Failed to fetchnet::ERR_NAME_NOT_RESOLVEDNo
Self-signed / untrusted certificateTypeError: Failed to fetchnet::ERR_CERT_AUTHORITY_INVALIDNo HTTP request (TLS failed)
Mixed content (https page, http API)TypeError: Failed to fetch"Mixed Content: The page at 'https://app.test:8984/' was loaded over HTTPS, but requested an insecure resource '...'. This request has been blocked"No
Ad blocker / extension (route.abort('blockedbyclient'))TypeError: Failed to fetchnet::ERR_BLOCKED_BY_CLIENTNo
Generic block (route.abort() default)TypeError: Failed to fetchnet::ERR_FAILEDNo
Page CSP connect-src 'self'TypeError: Failed to fetch"violates the following Content Security Policy directive: "connect-src 'self'"" and "Refused to connect because it violates the document's Content Security Policy."No
Browser offlineTypeError: Failed to fetchnet::ERR_INTERNET_DISCONNECTEDNo
Server closes socket with no responseTypeError: Failed to fetchnet::ERR_EMPTY_RESPONSEYes
Server sends TCP resetTypeError: Failed to fetchnet::ERR_CONNECTION_RESETYes
Unsafe port (fetch to port 6000)TypeError: Failed to fetchnet::ERR_UNSAFE_PORTNo (blocked before connecting)

Two things to note. First, with a missing CORS header the request did reach the server and the server answered 200. The browser just would not hand the answer to your code. So "it works in curl / Postman" is normal for a CORS bug. Second, a server-side 500 without CORS headers looks exactly like a CORS problem. Add CORS headers to error responses too, or you will chase the wrong bug.

4. Errors that look similar but have other wording

These did not say "Failed to fetch" in Chromium 153:

SituationWhat was thrown
HTTP 404 or 500 with CORS headersNothing. fetch() resolved; response.ok was false
AbortController.abort()DOMException AbortError: "signal is aborted without reason"
AbortSignal.timeout(200)DOMException TimeoutError: "signal timed out"
Invalid URLTypeError: "Failed to execute 'fetch' on 'Window': Failed to parse URL from ..."
HTML returned instead of JSONSyntaxError: "Unexpected token '<', "<!doctype "... is not valid JSON"
mode: 'no-cors'Nothing. You get an opaque response: type "opaque", status 0, empty body

One more trap: when the body was cut short (Content-Length 100, only 3 bytes sent), fetch() itself resolved with status 200. The error came later, from await response.text(), as TypeError: Failed to fetch with net::ERR_CONTENT_LENGTH_MISMATCH in the console. If the stack trace points at .json() or .text(), suspect a dropped connection mid-body.

5. The same causes in Node.js fetch

Node 24's built-in fetch uses a different message, TypeError: fetch failed, but it does give you the real reason in error.cause. There is no CORS in Node: the no-CORS-header request simply returned 200.

try { await fetch(url); }
catch (e) { console.log(e.message, e.cause?.code, e.cause?.message); }
Causee.messagee.cause.codee.cause.message
Connection refusedfetch failedECONNREFUSEDconnect ECONNREFUSED 127.0.0.1:8984
DNS failurefetch failedENOTFOUNDgetaddrinfo ENOTFOUND does-not-exist.invalid
Self-signed certificatefetch failedDEPTH_ZERO_SELF_SIGNED_CERTself-signed certificate; if the root CA is installed locally, try running Node.js with --use-system-ca
Server closes socketfetch failedUND_ERR_SOCKETother side closed
TCP resetfetch failedECONNRESETread ECONNRESET
Body cut short (on .text())terminatedUND_ERR_SOCKETother side closed
Port 6000fetch failed(none)bad port
AbortSignal.timeout(200)TimeoutError: "The operation was aborted due to timeout" (no cause)
Invalid URLFailed to parse URL from ...ERR_INVALID_URLInvalid URL

So in Node, always log e.cause. Logging only e.message throws the useful part away.

6. How to tell them apart and fix each one

Console showsCauseFix
"blocked by CORS policy: No 'Access-Control-Allow-Origin'"API does not allow your originSend Access-Control-Allow-Origin from the API, on every response including errors. For more than one site see CORS: allow multiple origins.
"Response to preflight request doesn't pass access control check"OPTIONS request fails or lacks CORS headersAnswer OPTIONS with 204 and CORS headers before auth or routing rejects it.
"Method X is not allowed by Access-Control-Allow-Methods"Preflight does not list the methodAdd it to Access-Control-Allow-Methods (or headers to Access-Control-Allow-Headers).
net::ERR_CONNECTION_REFUSEDNothing listening on that host/portStart the server; check the port and that it binds to the right interface (not only 127.0.0.1 in a container).
net::ERR_NAME_NOT_RESOLVEDHostname typo or missing DNS recordFix the URL or DNS. Check the record exists with dig or nslookup.
net::ERR_CERT_*Untrusted, expired or wrong-name certificateUse a valid certificate. Open the API URL directly in a tab to see the cert warning. See check SSL certificate expiry.
"Mixed Content ... This request has been blocked"https page calling an http URLServe the API over HTTPS and use an https:// URL.
net::ERR_BLOCKED_BY_CLIENTAd blocker or privacy extensionTest in a clean profile. Avoid URL paths like /ads/, /track, /analytics, which filter lists often block.
"violates the following Content Security Policy directive"Your page's CSP connect-srcAdd the API origin to connect-src. See CSP header examples.
net::ERR_INTERNET_DISCONNECTEDUser is offlineShow an offline message; retry later.
net::ERR_EMPTY_RESPONSE / ERR_CONNECTION_RESETServer crashed or a proxy dropped the connectionCheck server logs for a crash at that time. See ERR_CONNECTION_RESET / ECONNRESET.
net::ERR_CONTENT_LENGTH_MISMATCHResponse cut off mid-bodyFind what kills the response (timeouts, crashes, a wrong Content-Length).

7. A quick debugging order

  1. Open DevTools > Console and read the line just before or after the error. It names the real cause.
  2. Open the Network tab, click the failed request, and look at the Status column (e.g. (failed) net::ERR_... or CORS error).
  3. Run the same request with curl -i. If curl gets a normal answer, it is a browser-side rule: CORS, mixed content, CSP or an extension.
  4. If curl fails too, it is the network or server: DNS, refused, cert or reset.
  5. Check the API's response headers with our HTTP header checker to see if CORS headers are there.

Frequently asked questions

Why does fetch say 'Failed to fetch' with no details?

Browsers hide the reason from JavaScript so pages cannot probe networks. In our Chromium 153 tests, 15 different causes gave the same message and e.cause was undefined. The real reason is in the DevTools console and Network tab.

Is 'Failed to fetch' always a CORS error?

No. CORS is common, but we got the identical error from a server that was down, a DNS failure, a bad certificate, mixed content, an ad blocker, CSP, being offline and a dropped connection. Read the console line to tell them apart.

Why does the request work in curl or Postman but fail in the browser?

CORS, mixed content and CSP are browser rules. In our CORS test the server received the request and answered 200, but Chromium refused to give the response to the page. curl does not apply those rules.

Does a 404 or 500 cause 'Failed to fetch'?

Not by itself. With CORS headers present, fetch resolved and response.ok was false. But a 500 without CORS headers threw 'Failed to fetch' with a CORS message, because error pages often skip the CORS headers.

What is the Node.js version of this error?

Node 24 throws 'TypeError: fetch failed'. Unlike the browser, it puts the reason in e.cause: for example code ECONNREFUSED, ENOTFOUND, ECONNRESET, UND_ERR_SOCKET or DEPTH_ZERO_SELF_SIGNED_CERT.

Written with AI assistance. Measured on 2026-10-02 with headless Chromium 153.0.8010.12 (Playwright) and Node.js 24.21.0 against local Node HTTP and HTTPS (self-signed) test servers. Hostnames were mapped with --host-resolver-rules so the page and API were different origins. Ad blockers were simulated with Playwright route.abort(). PlainHeaders is our extension; Chromium, Playwright and Node.js are free.