Skip to content

Fix: TypeError: Failed to fetch (JavaScript, Browser)

FixDevs ·

Part of:  React & Frontend Errors

Quick Answer

Fix 'TypeError: Failed to fetch' by telling apart CORS, mixed content, an ad blocker, and being offline using the browser's Network tab, not the JS error alone.

I wasted a good chunk of an afternoon once treating this as a CORS bug because that’s what it usually is, before noticing the request was not even reaching the Network tab, an ad blocker extension was eating it before it left the page. In my experience the frustrating part of Failed to fetch is that the browser will not tell you why in the one place JavaScript can actually read, the caught error itself is identical no matter which of five or six unrelated problems caused it. This walks through how to actually tell them apart, starting with the one tool that has the answer: the Network tab, not the exception.

TypeError: Failed to fetch

Your fetch() call rejects with this in Chrome and Edge:

Uncaught (in promise) TypeError: Failed to fetch
    at fetchData (app.js:12)

Firefox reports the identical failure with different wording:

TypeError: NetworkError when attempting to fetch resource.

Safari uses a third, shorter phrasing for the same thing:

TypeError: Load failed

All three mean exactly the same thing: the request never completed at the network layer. The browser did not get as far as receiving an HTTP response, so there is no status code and no response body, and nothing else on the error object worth inspecting. Unlike Node’s server-side fetch, which attaches a detailed error.cause (see Fix: TypeError: fetch failed (Node.js)), the browser gives your script nothing more specific than the message itself.

Why the browser won’t tell you why

This is a deliberate design decision, not a bug in your code or a limitation someone forgot to fix. A fetch() rejection is the browser’s way of reporting a class of failures that span very different root causes, DNS failure, TLS failure, a CORS rejection, a Content-Security-Policy block, a browser extension intercepting the request, being offline, a connection reset. The specification does not require browsers to expose which one occurred to page JavaScript, and for the most common cause, CORS, this is explicitly intentional: if the browser told your script exactly why a cross-origin request was blocked, that disclosure itself could leak information about a server the attacker’s script has no right to see (whether an internal endpoint exists, requires auth, responds differently to different origins).

So the browser prints a much more specific message, but only to the DevTools console, for your eyes, never to your catch block:

Access to fetch at 'https://api.example.com/data' from origin 'https://yoursite.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

That line is gold and your code will never see it. The generic TypeError: Failed to fetch is all catch (err) ever gets. This is why triage has to start outside the exception.

Triage with the Network tab first, not the error

Open DevTools, go to the Network tab, and re-trigger the request before doing anything else. What you see there narrows the cause immediately:

What the Network tab showsLikely cause
The request never appears at allA browser extension (ad blocker, privacy tool) blocked it before it left the page
Request appears, status shows (failed) net::ERR_BLOCKED_BY_CLIENTConfirmed: an extension blocked it
Request appears but never completes, page also shows a mixed-content warning in the consoleAn HTTPS page tried to fetch a plain http:// URL
Request appears, status (blocked:csp)Your own Content-Security-Policy connect-src rule blocked it
A preflight OPTIONS request appears and fails, or the real request appears with no CORS response headersCORS: the server did not authorize your origin
The request shows (failed) net::ERR_INTERNET_DISCONNECTED or net::ERR_NAME_NOT_RESOLVEDGenuinely offline, or DNS could not resolve the host
The request completes with a real status code (404, 500, etc.)This is not Failed to fetch at all, that is a normal HTTP response your code should read, not a rejected promise

That last row matters: if a status code shows up in the Network tab, fetch() did not reject, it resolved, and whatever threw came from your own response-handling code, not the network layer.

Fix CORS: the most common cause

Confirm it with the console message described above, then fix it on the server, since a client-side workaround for CORS does not exist by design. The server must send the header naming your origin:

Access-Control-Allow-Origin: https://yoursite.com

or, for public APIs with no credentials involved:

Access-Control-Allow-Origin: *

If your request sends cookies or an Authorization header (credentials: 'include'), the wildcard is rejected by the spec, the server must echo back your specific origin and also send Access-Control-Allow-Credentials: true. See Fix: Express CORS Not Working and Fix: Next.js CORS Error for the server-side configuration in those two stacks specifically.

A failed preflight is the same symptom with an extra step. Any cross-origin request with a non-simple method (PUT, DELETE, PATCH) or custom headers triggers a preflight OPTIONS request first. If the server does not answer that OPTIONS request with the right CORS headers, the browser never sends your actual request at all, and you still get Failed to fetch, with an OPTIONS row failing in the Network tab as the only clue.

Fix mixed content: HTTPS page, HTTP request

Browsers silently block active content, including fetch(), from an HTTPS page to a plain http:// URL. This is not a CORS problem and adding CORS headers will not fix it. Two real fixes:

// WRONG on an HTTPS page: silently blocked
fetch('http://api.example.com/data');

// CORRECT: the target must also serve HTTPS
fetch('https://api.example.com/data');

If the target genuinely has no HTTPS available (an old internal tool, an IoT device on the local network), there is no client-side override; the target has to be reachable over HTTPS, or proxied through something that is.

Fix ad blocker and extension interference

Browser extensions that block by URL pattern are the cause that looks most like a bug in your own code, because the request simply never appears in the Network tab. Content blockers match on substrings and known tracker lists, so URLs containing words like analytics, ads, pixel, tracker, or matching a known third-party domain get blocked even when the endpoint is your own first-party API that happens to share a naming pattern. I’ve seen this exact false alarm in a support ticket where the customer’s own ad blocker was quietly eating requests to an endpoint named /api/ad-inventory, nothing to do with third-party ads at all.

Confirm it by disabling extensions (an incognito window with extensions off is the fastest test) and retrying. If confirmed, the only real fixes are renaming the offending endpoint or asset path away from ad-adjacent keywords, or accepting that a fraction of users with aggressive blockers will fail this specific request and building a fallback.

Fix CSP connect-src blocks

If your page sets a Content-Security-Policy and connect-src does not list the target origin, the browser blocks the fetch before it leaves, and the Network tab shows (blocked:csp) with a corresponding console violation. Add the origin to the directive:

Content-Security-Policy: connect-src 'self' https://api.example.com;

This is easy to miss because it only appears in production if your CSP is set by a proxy, CDN, or meta tag that differs from your local dev environment, so the failure can be entirely invisible until deployment.

Fix genuine network failures

net::ERR_INTERNET_DISCONNECTED and net::ERR_NAME_NOT_RESOLVED in the Network tab mean exactly what they say: no connectivity, or DNS could not resolve the hostname. Check the actual network connection and the URL for typos before assuming anything more complex is wrong. navigator.onLine can give an early signal, though it is not fully reliable (it reflects whether the OS reports a network interface as connected, not whether that connection can actually reach your target):

if (!navigator.onLine) {
  console.warn('Browser reports no network connection');
}

A newer, quieter cause: local network access restrictions

Chrome has been rolling out restrictions on requests from a public web page to localhost or a private IP address (a local dev server, a device on your home network), under names that have changed as the feature evolved, Private Network Access and, more recently, Local Network Access. The exact enforcement has shifted across Chrome versions and is still rolling out as of this writing, in some versions it required the local server to answer an extra CORS preflight header, in newer versions it surfaces as a browser permission prompt instead of a silent failure. If a public site’s fetch() to your local dev server on localhost or 192.168.x.x fails, or a permission prompt appears where you did not expect one, this restriction, not CORS in the traditional sense, is usually why. Check chrome://flags for local-network-access-check if you need to reproduce or rule this out on a specific Chrome version.

Still not working?

A timeout with no built-in signal. fetch() has no default timeout, a hung request simply never resolves or rejects, it is not the same failure as Failed to fetch. If you need one, use AbortController:

const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);

fetch(url, { signal: controller.signal }).catch(err => {
  if (err.name === 'AbortError') console.log('Request timed out');
});

Service worker interference. If a service worker is registered for the page, it can intercept fetch events and fail in ways that look identical to a network error, especially after a service worker update leaves a stale or broken cache strategy active. Check the Application tab in DevTools for an active service worker and try a hard reload with “Bypass for network” enabled to rule it out.

The response body is HTML instead of JSON, and this isn’t actually Failed to fetch. If the request succeeds with a real status code but your app crashes trying to parse the body, that is a different, unrelated error. See Fix: Unexpected token ’<’ in JSON for that one specifically, it means the server responded, just not with what you expected.

Works in one browser, fails in another. Given how differently Chrome, Firefox, and Safari phrase this error, and enforce features like Local Network Access, always reproduce in the specific browser your users report before assuming the cause is the same one you found first.

It only fails after a production deploy. A CSP header, a reverse proxy stripping CORS headers, or an environment-specific API URL are the usual differences between a working dev environment and a broken production one. Diff the response headers between environments with your browser’s Network tab rather than guessing.

Failed to fetch dynamically imported module is a specific variant, not a separate bug. Vite and code-split React/Vue apps load lazy chunks via import(), which the browser fetches like any other module. Deploy a new build while a user’s tab is still open on the old one, and their next import() targets a hashed filename (Home-d165e21c.js) that no longer exists on the server, producing TypeError: Failed to fetch dynamically imported module: <url>. This is the stale-deploy problem, not CORS or a network issue. See Fix: Loading Chunk Failed for the fix (typically catching the error and forcing a reload).

For the equivalent server-side error and its very different set of causes, see Fix: TypeError: fetch failed (Node.js). For related networking and CORS errors, see Fix: Express CORS Not Working and Fix: Next.js CORS Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles