How to Fix "Failed to Fetch" Errors in JavaScript
"TypeError: Failed to fetch" means the fetch() promise rejected — the request never completed at all. Here is why that is different from a 404, what actually stops the request, and how to find the real reason.
Uncaught (in promise) TypeError: Failed to fetch is one of the most frustrating errors in front-end JavaScript, because the message tells you almost nothing. It is the same string whether your API is down, your page is offline, a browser extension ate the request, or you typed the URL wrong. The one thing it always tells you is the most important thing — and almost everyone misses it: the fetch() promise rejected. The request did not come back with a bad answer. It never came back at all.
That single distinction is the whole guide. Once you internalise what "rejected" means for fetch(), the vague message becomes a precise category of problem, and the Network tab hands you the specific cause. Let us make the distinction load-bearing before touching a single fix.
What "Failed to fetch" actually means
TypeError: Failed to fetch means the fetch() promise rejected because the request failed at the network level — it never completed. It is not an HTTP status error: a 404 or 500 still resolves the promise with response.ok === false. "Failed to fetch" specifically means something — CORS, offline, mixed content, a block, a bad URL — stopped the request from completing at all.
MDN is explicit about this: the promise returned by fetch() rejects only when the request cannot be made or the response cannot be retrieved — a genuine network error. It does not reject because the server answered with an error status. If the browser managed to send the request and read a response back, the request completed, and from fetch()'s point of view that is a success, no matter what the status code says.
So "Failed to fetch" is not "the server said no." It is "the browser never got a usable answer." The connection was refused, blocked, redirected into a wall, or cut off. The message is intentionally generic — the browser withholds specifics to avoid leaking cross-origin information — which is exactly why you cannot debug this from the console string alone.
Reject vs. resolve: the distinction that trips everyone up
Here is the trap that costs people hours. A developer writes await fetch(url), wraps it in try/catch, and assumes the catch block will run whenever "something goes wrong." It will not. A 404, a 403, a 500 — none of them throw. The await resolves cleanly, control skips the catch entirely, and the code marches on treating an error page as if it were valid data.
That is because an unsuccessful HTTP response is still a response. The promise resolves with a Response object whose ok property is false (it is true only for statuses in the 200–299 range) and whose status holds the real code. The catch block fires only for the other category — the network-level failure that produces "Failed to fetch." These are two different failure modes, and robust code has to handle both, separately.
// Failure mode 1 — the request COMPLETED but with an error status.
// fetch() RESOLVES. The catch below never runs.
const res = await fetch('/api/user/999'); // 404 Not Found
console.log(res.ok); // false
console.log(res.status); // 404
// ...but no exception was thrown. This is NOT "Failed to fetch".
// Failure mode 2 — the request never COMPLETED (offline, CORS, blocked).
// fetch() REJECTS. This is the one that throws "Failed to fetch".
try {
await fetch('https://api.down-or-blocked.example/data');
} catch (err) {
console.log(err.name); // 'TypeError'
console.log(err.message); // 'Failed to fetch'
}The causes of "Failed to fetch"
Because the message is one-size-fits-all, it covers a whole family of network-level failures. These are the ones you will actually hit, roughly in order of frequency:
- CORS. The browser blocked the request (or its preflight) because the server did not return an
Access-Control-Allow-Originheader authorising your origin. This is common enough that it deserves its own playbook — see how to debug CORS errors for reading the failing preflight and applying the exact server-side header. But note: CORS is one cause of "Failed to fetch," not the only one. - Offline or DNS failure. The device has no connection, or the hostname cannot be resolved. The request never leaves the machine.
- Mixed content. An
httpspage requesting anhttpURL. The browser blocks or upgrades the insecure request; the plain-http version is refused outright. - A wrong or unreachable URL. A typo, a stale endpoint, a port nothing is listening on — the connection is refused and nothing comes back.
- The server is down. The host is reachable but the service is not accepting connections, so there is no response to read.
- A block by an extension, ad-blocker, or CSP. An ad-blocker or privacy extension cancels the request, or your own
Content-Security-Policyconnect-srcdirective forbids the origin. - An aborted request. An
AbortControllerfired (a timeout, a component unmounting) and cancelled the request mid-flight.
// Mixed content: page is https, request is http → blocked as insecure.
// Fix: request the https URL (or a protocol-relative / same-origin path).
await fetch('http://api.example.com/data'); // blocked → Failed to fetch
await fetch('https://api.example.com/data'); // ok
// Wrong / unreachable URL: nothing is listening → connection refused.
await fetch('http://localhost:3001/api'); // dev server is on :3000
// Fix: validate the base URL and port before shipping.
const API = new URL('/api', window.location.origin); // build it, don't hardcodeHow to find the real reason
The console gives you the category; the Network tab gives you the cause. Open DevTools, switch to Network, and reproduce the request. Find the failing row — its status will read (failed) or CORS error rather than a number — and click into it. What you see there is the actual diagnosis: a CORS message names the missing header, a mixed-content block says the request was blocked as insecure, an ad-blocker or CSP block shows the request cancelled before it went out, and an offline or DNS failure shows a connection that never opened. Two failures that print the identical console string look completely different here.
The most important habit, though, lives in the code itself: check response.ok separately, so you never confuse an HTTP error with a network failure. The example below handles both modes explicitly — the rejected promise (network failure) in the catch, and the resolved-but-unsuccessful response (!response.ok) with its own branch.
async function getData(url) {
// Cheap early signal: are we even online?
if (!navigator.onLine) {
throw new Error('You appear to be offline. Check your connection.');
}
let response;
try {
response = await fetch(url);
} catch (err) {
// fetch() REJECTED → a network-level failure ("Failed to fetch").
// CORS, offline, DNS, mixed content, blocked, aborted, server down.
throw new Error(`Network request failed: ${err.message}`);
}
// fetch() RESOLVED, but the HTTP status might still be an error.
// This is a SEPARATE check — a 404/500 never reaches the catch above.
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
return response.json();
}
// Optional: react to connectivity changes in the UI.
window.addEventListener('offline', () => showBanner('You are offline'));
window.addEventListener('online', () => hideBanner());Two branches, two meanings. The catch is where "Failed to fetch" lands — treat it as a connectivity or blocking problem, not a data problem. The !response.ok branch is where 404s and 500s land — the server answered, and you can show a real status. Collapsing the two into one generic "something went wrong" is how a blocked request and a missing record end up indistinguishable to your users and to you.
When it only happens for someone else
The Network tab is perfect until the failure is not on your machine. A user reports "the page won't load data," you open your own DevTools, and everything works — because the cause was their ad-blocker, their flaky connection, their corporate proxy stripping the request, or a CORS rule that only bites from their origin. The evidence you need lives in a Network tab you will never see, and all you get is a screenshot of a bare "Failed to fetch."
This is the gap BugMojo closes. The browser extension captures the failing request as a structured network entry — the URL, the method, the status, whether it was blocked, refused, or CORS-rejected — alongside the console error and a replay of what the user did. Instead of a screenshot of a generic message, you get the exact request that failed and the reason it failed, from the environment where it actually happened. And because that capture is handed to an AI coding agent over MCP, the agent reads the real network failure and names the fix, rather than guessing which of eight causes produced the same vague string.
Install the free BugMojo extension to capture the failing network request — status, block reason, CORS detail — with the console error and a session replay, so "Failed to fetch" stops being a guessing game. No project setup required.
Install the free extensionFrequently asked questions
Frequently asked questions
Sources
- Using the Fetch API — how to make requests, read responses, and handle errors including the difference between a rejected promise and an unsuccessful response — MDN Web Docs (2026)
- Window: fetch() method — the returned promise rejects only on a network error, not on an HTTP error status such as 404 or 500 — MDN Web Docs (2026)
- Response: ok property — true only for statuses in the 200–299 range; an HTTP error still resolves the fetch with ok set to false — MDN Web Docs (2026)
- Mixed content — an https page that requests an http resource has the request blocked or upgraded by the browser — MDN Web Docs (2026)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

