TypeError: Failed to fetch: what it means and how to find the cause

TL;DR

"Failed to fetch" is the TypeError Chrome and Edge throw when a fetch() request never gets a usable HTTP response, because it was blocked, cancelled or couldn't connect. It isn't a status code, so a 404 or a 500 never causes it. Firefox reports the same failure as "NetworkError when attempting to fetch resource", and Safari says "Load failed".

In this guide

Why the error message doesn't tell you the cause

Every cause of a failed fetch produces the same TypeError in your code, on purpose. If JavaScript could tell a CORS rejection from a refused connection, any website could use your browser to probe servers it isn't allowed to read, including ones on your private network. So the page gets one vague error, and the real reason goes somewhere only a person can see it.

Here's what Chrome gave the page's own code for four very different failures:

text
CORS rejection        TypeError: Failed to fetchUnknown host          TypeError: Failed to fetchConnection refused    TypeError: Failed to fetchMistyped URL scheme   TypeError: Failed to fetch

And here's what it printed in the console at the same moment, with the addresses shortened:

text
Access to fetch at 'https://api.example.com/orders' from origin 'https://shop.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.Failed to load resource: net::ERR_NAME_NOT_RESOLVEDFailed to load resource: net::ERR_CONNECTION_REFUSEDFetch API cannot load htp://example.com/. URL scheme "htp" is not supported.

That's the whole problem in two code blocks. The answer is always in the console or the Network tab, never in error.message. Which also means an error tracker that only stores the message is storing "Failed to fetch" a thousand times and telling you nothing. Log the URL and method alongside it at the very least.

The causes, and how to recognise each one

Six causes cover nearly every case. The console lines are Chrome's, and Edge prints the same ones. Firefox words them differently and, confusingly, blames CORS for some failures that have nothing to do with it: an unknown host comes out as "CORS request did not succeed".

CauseWhat Chrome's console saysWhat to check
CORShas been blocked by CORS policy and the missing headerThe API's response headers, for this origin
Offline, VPN or DNSnet::ERR_INTERNET_DISCONNECTED or net::ERR_NAME_NOT_RESOLVEDWhether the host resolves from that network
Blocked by an extensionnet::ERR_BLOCKED_BY_CLIENTRetry in a private window with extensions off
Navigation cancelled itOften nothing. The Network tab shows the request as (canceled)A link or form that left the page mid-request
Mixed contentMixed Content: The page at '…' was loaded over HTTPS, but requested an insecure resourceAn http:// URL on an https:// page
Bad URL or portnet::ERR_CONNECTION_REFUSED, or URL scheme "…" is not supportedTypos, a missing https://, a local API that isn't running

CORS is the one that wastes the most time, because it looks like a network error and is really a server configuration. The request often reached your API and got a perfectly good answer. The browser just refused to hand it to the page. curl ignores CORS, which makes it the quickest test: if curl gets a 200 and the browser says Failed to fetch, the request works and the headers don't.

Extensions are the one support teams should suspect first for a single customer. Ad and privacy blockers match on URL patterns, so an endpoint called /api/track-order or /analytics/summary can be blocked for that customer and fine for everyone else.

Cancelled navigation catches out a lot of forms. A form whose submit handler calls fetch but never calls event.preventDefault() starts the request, then the browser reloads the page and kills it. The console shows Failed to fetch for a split second, if you catch it at all.

How to find the cause step by step

  1. Open the developer tools, switch to the Network tab, and tick Preserve log so a page change doesn't wipe the evidence.
  2. Reproduce the error, then find the red request. Its status column says CORS error, (blocked:mixed-content), (canceled) or (failed).
  3. Read the matching line in the console. It names the reason the page couldn't see.
  4. Try the same URL with curl, and in a private window. That separates the server, the browser and the extensions in two goes.

To check CORS from the command line, send the preflight request the browser would send:

Shell
curl -i -X OPTIONS https://api.example.com/orders \  -H "Origin: https://shop.example.com" \  -H "Access-Control-Request-Method: POST" \  -H "Access-Control-Request-Headers: content-type"

The response needs an Access-Control-Allow-Origin header naming your origin (or *, if the request sends no cookies) and an Access-Control-Allow-Headers header that includes content-type. If either is missing, the fix is on the API.

When the error comes from a customer

You can't open the console on someone else's computer, and the customer usually can't tell you what it says. Ask which browser and extensions they use, and whether they're on a VPN or a work network. Then ask for a HAR file, which records every request including the failed one. The HAR checker reads one and strips the secrets before you share it. If you'd like to try the console yourself first, here's how to open it in every browser.

Handling it in your code

Catch it, keep the context, and don't pretend it's always the customer's connection:

JavaScript
async function getJSON(url, options) {  let res;  try {    res = await fetch(url, options);  } catch (err) {    if (err.name === 'AbortError') throw err; // cancelled on purpose    throw new Error(`Request to ${url} failed before a response arrived`, { cause: err });  }  if (!res.ok) throw new Error(`${url} returned ${res.status}`);  return res.json();}

A "check your internet connection" message for every Failed to fetch is often simply wrong. navigator.onLine === false reliably means offline, but true only means there's a network interface, not that your API is reachable. Show the connection message when you know, and a plain "something went wrong, try again" when you don't.

For support teams

Ask the customer to try once in a private window. If that works, an extension is the likely cause, and it's worth naming it in the ticket so engineering can rename the blocked endpoint rather than chase a bug that only exists in one browser.

More on Console errors

Found this useful? Pass it on to your team.