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
| Cause | fetch() threw | Console / Network shows | Reached server? |
|---|---|---|---|
| CORS: no Access-Control-Allow-Origin | TypeError: Failed to fetch | "has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource." then net::ERR_FAILED | Yes, server sent 200 |
| CORS preflight gets 404 | TypeError: Failed to fetch | "Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header..." then net::ERR_FAILED | Only the OPTIONS request |
| CORS preflight: method not allowed | TypeError: Failed to fetch | "Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response." | Only OPTIONS |
| Server returns 500 without CORS headers | TypeError: Failed to fetch | Same "No 'Access-Control-Allow-Origin' header" CORS line | Yes |
| Server down (connection refused) | TypeError: Failed to fetch | net::ERR_CONNECTION_REFUSED | No |
| DNS name does not exist | TypeError: Failed to fetch | net::ERR_NAME_NOT_RESOLVED | No |
| Self-signed / untrusted certificate | TypeError: Failed to fetch | net::ERR_CERT_AUTHORITY_INVALID | No 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 fetch | net::ERR_BLOCKED_BY_CLIENT | No |
| Generic block (route.abort() default) | TypeError: Failed to fetch | net::ERR_FAILED | No |
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 offline | TypeError: Failed to fetch | net::ERR_INTERNET_DISCONNECTED | No |
| Server closes socket with no response | TypeError: Failed to fetch | net::ERR_EMPTY_RESPONSE | Yes |
| Server sends TCP reset | TypeError: Failed to fetch | net::ERR_CONNECTION_RESET | Yes |
| Unsafe port (fetch to port 6000) | TypeError: Failed to fetch | net::ERR_UNSAFE_PORT | No (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:
| Situation | What was thrown |
|---|---|
| HTTP 404 or 500 with CORS headers | Nothing. fetch() resolved; response.ok was false |
| AbortController.abort() | DOMException AbortError: "signal is aborted without reason" |
| AbortSignal.timeout(200) | DOMException TimeoutError: "signal timed out" |
| Invalid URL | TypeError: "Failed to execute 'fetch' on 'Window': Failed to parse URL from ..." |
| HTML returned instead of JSON | SyntaxError: "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); }
| Cause | e.message | e.cause.code | e.cause.message |
|---|---|---|---|
| Connection refused | fetch failed | ECONNREFUSED | connect ECONNREFUSED 127.0.0.1:8984 |
| DNS failure | fetch failed | ENOTFOUND | getaddrinfo ENOTFOUND does-not-exist.invalid |
| Self-signed certificate | fetch failed | DEPTH_ZERO_SELF_SIGNED_CERT | self-signed certificate; if the root CA is installed locally, try running Node.js with --use-system-ca |
| Server closes socket | fetch failed | UND_ERR_SOCKET | other side closed |
| TCP reset | fetch failed | ECONNRESET | read ECONNRESET |
| Body cut short (on .text()) | terminated | UND_ERR_SOCKET | other side closed |
| Port 6000 | fetch failed | (none) | bad port |
| AbortSignal.timeout(200) | TimeoutError: "The operation was aborted due to timeout" (no cause) | ||
| Invalid URL | Failed to parse URL from ... | ERR_INVALID_URL | Invalid 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 shows | Cause | Fix |
|---|---|---|
| "blocked by CORS policy: No 'Access-Control-Allow-Origin'" | API does not allow your origin | Send 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 headers | Answer 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 method | Add it to Access-Control-Allow-Methods (or headers to Access-Control-Allow-Headers). |
| net::ERR_CONNECTION_REFUSED | Nothing listening on that host/port | Start 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_RESOLVED | Hostname typo or missing DNS record | Fix the URL or DNS. Check the record exists with dig or nslookup. |
| net::ERR_CERT_* | Untrusted, expired or wrong-name certificate | Use 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 URL | Serve the API over HTTPS and use an https:// URL. |
| net::ERR_BLOCKED_BY_CLIENT | Ad blocker or privacy extension | Test 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-src | Add the API origin to connect-src. See CSP header examples. |
| net::ERR_INTERNET_DISCONNECTED | User is offline | Show an offline message; retry later. |
| net::ERR_EMPTY_RESPONSE / ERR_CONNECTION_RESET | Server crashed or a proxy dropped the connection | Check server logs for a crash at that time. See ERR_CONNECTION_RESET / ECONNRESET. |
| net::ERR_CONTENT_LENGTH_MISMATCH | Response cut off mid-body | Find what kills the response (timeouts, crashes, a wrong Content-Length). |
7. A quick debugging order
- Open DevTools > Console and read the line just before or after the error. It names the real cause.
- Open the Network tab, click the failed request, and look at the Status column (e.g.
(failed) net::ERR_...orCORS error). - 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. - If curl fails too, it is the network or server: DNS, refused, cert or reset.
- 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.