TypeError: Failed to fetch

This is the least helpful error in web development, and that is on purpose: telling your page why a request failed would let it probe other sites. So the browser says nothing useful to your code — and puts the real reason somewhere else. Here is where, and how to tell the five causes apart.

On this page
  1. The thirty-second check
  2. Cause 1: CORS blocked it
  3. Cause 2: nothing is listening
  4. Cause 3: the URL is not what you think
  5. Cause 4: mixed content
  6. Cause 5: something blocked the request
  7. What to do in the code

The thirty-second check

Open DevTools, go to the Network tab, and run the request again. Then read one column:

What the Network tab showsWhat it means
The request is there, with a status like 200The request worked. The browser is refusing to let your page read it — that is CORS.
The request is there, status (failed) or (blocked)Nothing answered, or something blocked it.
There is no request at allIt never left the page: mixed content, or a malformed URL.
Status (canceled)Your own code aborted it — an AbortController, a timeout, or a component unmounting.

Also look just above and below the TypeError in the console. The browser usually prints a second, far more specific message there — the one that actually names CORS or mixed content. The TypeError is what your catch block sees; the other line is what the browser wanted you to read.

Cause 1: CORS blocked it

The commonest cause in local development, and the one that confuses people most, because the request succeeded. The server got it, did the work and answered. The browser then refused to hand the answer to your page, because the response did not say your page was allowed to read it.

Tell-tale signs: the Network tab shows a normal status code, and there is a separate console line mentioning Access-Control-Allow-Origin or “blocked by CORS policy”.

That is a whole subject of its own, including why it happens between two localhost ports: CORS error: what it means and how to fix it. You can paste your exact error there and it will tell you which of the CORS cases you have. There is also a CORS tester for checking what a server actually sends back.

Cause 2: nothing is listening

The API is not running, crashed on boot, or is on a different port than you remember. This looks alarming and is usually the easiest fix.

Check it outside the browser, where there is no CORS and no extension in the way:

curl -i http://localhost:3000/api/health

If curl also fails to connect, the problem is the server, not your front end. If curl succeeds and the browser does not, you are probably looking at CORS.

Cause 3: the URL is not what you think

A template literal that produced undefined, a missing slash, a base URL that is empty in this environment. Print the URL immediately before the call rather than reading the code:

const url = `${base}/api/items`;
console.log("fetching", url);       // fetching undefined/api/items
const res = await fetch(url);

A URL the browser cannot parse at all throws before any request is made, which is why the Network tab stays empty.

Cause 4: mixed content

An https:// page is not allowed to fetch http://. The browser blocks it before it leaves, and the Network tab shows nothing. The console line says “Mixed Content”.

This catches people whose local API is plain http while the front end is served over https, and people who hard-coded http:// in a config that is now deployed.

Cause 5: something blocked the request

An ad blocker, a privacy extension, a corporate proxy or a VPN can refuse a request before it reaches the network. Requests to anything whose path or host looks like analytics are the usual casualties — a file called ads.js or a path containing /track can be blocked even when it is yours.

Test it in a clean profile with no extensions, or an incognito window with extensions disabled. If it works there, an extension is the answer, and the Network tab usually shows the request as (blocked:...).

What to do in the code

One thing worth knowing: fetch does not reject on 404 or 500. Those are successful round trips as far as it is concerned. A TypeError means the request never produced a readable response at all, which is why checking res.ok and catching are two different jobs:

try {
  const res = await fetch(url);
  if (!res.ok) {
    // The server answered, and said no. 404, 500, and so on.
    throw new Error(`${res.status} ${res.statusText}`);
  }
  return await res.json();
} catch (err) {
  // TypeError: Failed to fetch lands here -- and so does the throw above.
  // They are different problems; tell them apart before reporting.
  console.error("request failed", url, err);
  throw err;
}

If you are testing against a server you cannot change, you can add the missing CORS headers in your own browser while you work — see the testing section of the CORS guide. That fixes nothing for your users, and it cannot rescue a preflight the server rejects, which is exactly the case the guide explains.

More on this topic: net::ERR_BLOCKED_BY_CLIENT, ERR_BLOCKED_BY_RESPONSE and ERR_BLOCKED_BY_ORB · “Has been blocked by CORS policy”: every variant and its fix · Access-Control-Allow-Origin: values, multiple origins and server examples · What is CORS? Cross-Origin Resource Sharing, in plain words