# Fix: TypeError: Failed to fetch

`fetch()` rejects with this error when the browser could not get a usable response at all: the request was blocked, the server could not be reached, or the connection dropped. A 404 or 500 does not cause it. To fix it, open the failed request in the DevTools Network tab and read the real reason there. The most common ones are CORS, a server that is down or unreachable, mixed content and ad blockers.

| Fact | Detail |
|---|---|
| Error | `TypeError: Failed to fetch` (Chrome, Edge) |
| Firefox | `TypeError: NetworkError when attempting to fetch resource.` |
| Safari | `TypeError: Load failed` |
| Where you see it | Browser console, your `catch` block, error logs from the browser |
| Usual cause | CORS, server down or wrong URL, mixed content, ad blocker, offline |
| Usual fix | Find the reason in DevTools (console line above it, Network status column), then fix that |
| BugTape catches it | Yes, as a failed request when it follows a click within 8 seconds. Other requests need one line |

## What the error means

`fetch()` only rejects when the request fails. When the server answers with any HTTP status, including 404 or 500, the promise resolves and `res.ok` is `false`. When the browser cannot get a response it is allowed to give you, the promise rejects with a `TypeError`. The message is deliberately vague, so pages cannot probe other servers. Chrome says `Failed to fetch`, Firefox says `NetworkError when attempting to fetch resource.` and Safari says `Load failed`.

The real reason is almost always printed elsewhere: a CORS or Mixed Content message in the console just above the error, or a `net::ERR_…` code in the Network tab (for example `ERR_CONNECTION_REFUSED`, `ERR_NAME_NOT_RESOLVED`, `ERR_BLOCKED_BY_CLIENT`, `ERR_INTERNET_DISCONNECTED`).

If the request was cancelled with an `AbortController`, `fetch()` rejects with an `AbortError` instead. That is a different error.

## Common causes

1. **CORS.** The API is on another origin and does not send `Access-Control-Allow-Origin` for your site, or does not answer the `OPTIONS` preflight. The console shows `… has been blocked by CORS policy`.
2. **The server is down, or the URL is wrong.** Nothing is listening on that host and port (`ERR_CONNECTION_REFUSED`), the hostname does not resolve (`ERR_NAME_NOT_RESOLVED`), or the API base URL points to `localhost` in production.
3. **Mixed content.** An HTTPS page calls an `http://` URL. The browser blocks it and logs `Mixed Content: … This request has been blocked`.
4. **An ad blocker or privacy extension.** URLs containing words like `analytics`, `track` or `ads` are blocked. The Network tab shows `ERR_BLOCKED_BY_CLIENT`.
5. **The user is offline or the connection dropped.** Mobile networks, VPNs and captive portals.
6. **An invalid request.** A URL with a username and password in it, a malformed URL, or an invalid option in the `fetch()` call.
7. **The page navigated away during the request.** The browser cancels in-flight requests when the page unloads.
8. **A TLS problem.** An expired or self-signed certificate on the API host (`ERR_CERT_…`).

## How to fix it

### Read the reason in DevTools

Open DevTools, then the Network tab, and repeat the action. Click the red row.

- Status `(failed)` with `net::ERR_CONNECTION_REFUSED` or `ERR_NAME_NOT_RESOLVED`: the server or URL. Check that the API is running and the URL is right.
- Status `(blocked:mixed-content)`: an `http://` URL on an HTTPS page.
- `ERR_BLOCKED_BY_CLIENT`: an extension. Try a private window with extensions off.
- `CORS error`, or a console line `blocked by CORS policy`: fix the headers on the API server.

### Fix CORS on the server

The server you call must allow your origin. In Express:

```js
import cors from 'cors';

app.use(cors({ origin: ['https://app.example.com'], credentials: true }));
```

In a Next.js route handler, return `Access-Control-Allow-Origin` on every response and export an `OPTIONS` handler. If the frontend and API are the same Next.js app, call a relative URL (`fetch('/api/data')`) and CORS does not apply. The full set of fixes is in [the CORS guide](/guides/cors-blocked-no-access-control-allow-origin/).

### Point production at the right API URL

A base URL that falls back to `localhost` works on your machine and fails for every user. Read it from an environment variable that is set at build time, and fail the build when it is missing.

```ts
// Vite: import.meta.env; Next.js: process.env.NEXT_PUBLIC_API_URL
const API_URL = import.meta.env.VITE_API_URL;
if (!API_URL) throw new Error('VITE_API_URL is not set');

const res = await fetch(`${API_URL}/items`);
```

Next.js and Vite inline these values when they build, so changing the variable needs a new build.

### Use HTTPS for the API

On an HTTPS page every `fetch()` URL must also be HTTPS. Change `http://` to `https://` in the URL or the environment variable. See [the mixed content guide](/guides/mixed-content-blocked/).

### Move first-party requests off blocked paths

Ad blockers match URL patterns. If your own endpoint is called `/api/track` or `/analytics/event`, rename it. You cannot stop users from running an extension, so handle the error and keep the page usable.

### Handle the error in your code

Treat a rejection and a bad status as two different failures, and tell the user something useful.

```ts
async function getItems() {
  let res: Response;
  try {
    res = await fetch('/api/items');
  } catch (err) {
    if ((err as Error).name === 'AbortError') return null; // we cancelled it
    throw new Error(`GET /api/items could not connect: ${(err as Error).message}`, { cause: err });
  }
  if (!res.ok) throw new Error(`GET /api/items returned ${res.status}`);
  return res.json();
}
```

### Retry when the user is offline

`navigator.onLine` is `false` when the browser knows it has no connection. Wait for the `online` event before you retry, and retry a small number of times with a delay.

```ts
async function fetchWithRetry(url: string, init?: RequestInit, tries = 3): Promise<Response> {
  for (let attempt = 1; ; attempt++) {
    try {
      return await fetch(url, init);
    } catch (err) {
      if (attempt >= tries || (err as Error).name === 'AbortError') throw err;
      if (!navigator.onLine) await new Promise((r) => window.addEventListener('online', r, { once: true }));
      else await new Promise((r) => setTimeout(r, 500 * attempt));
    }
  }
}
```

Only retry requests that are safe to repeat, such as `GET`, or `POST` requests that use an idempotency key.

### Cancel requests on purpose when the page changes

In React, cancel a request when the component unmounts. The rejection is then an `AbortError` that you can ignore.

```tsx
useEffect(() => {
  const controller = new AbortController();
  fetch('/api/items', { signal: controller.signal })
    .then((res) => res.json())
    .then(setItems)
    .catch((err) => { if (err.name !== 'AbortError') reportError(err); });
  return () => controller.abort();
}, []);
```

## How to catch it in production with BugTape

The BugTape browser SDK records every `fetch` and XHR. When one fails with a network error within 8 seconds of a click, BugTape reports it as **User-triggered request failed: /api/items (failed to fetch)**, with the method, the full request URL, the element the user clicked, the replay of the page and the other requests around it. The text in brackets is the browser's own message, so Firefox and Safari reports read `networkerror when attempting to fetch resource.` and `load failed`. Repeats of the same request from the same element become one issue with a count.

What BugTape does not report on its own:

- **Requests that are not tied to a click.** A request on page load, on a timer or from a background poll is recorded in the network list of any report, but does not start one. Catch the error and call `reportError(err)`, which fires the window `error` event the SDK listens for, as in the `useEffect` example above.
- **Aborted requests.** A request cancelled with `AbortController` is skipped, so unmounting components do not create noise.
- **Requests to BugTape itself and to common analytics, ads and monitoring hosts.** These are ignored.
- **Anything before `init()` runs.**

The CORS and Mixed Content lines are printed by the browser itself and do not pass through `console`, so they are not in the recorded console output. The request URL in the report usually shows the cause: another origin points to CORS, an `http://` URL to mixed content. An `await fetch()` with no `catch` is also reported, as **Unhandled promise rejection: Failed to fetch**.

```ts
import { init } from '@bugtape/sdk';

init({ apiKey: 'YOUR_PROJECT_KEY', release: import.meta.env.VITE_COMMIT_SHA });
```

Set `release` so a fix that does not hold shows up: when you mark the issue fixed, a report from a newer release reopens it as a regression. Your coding agent can read the issue over MCP (`npx -y bugtape connect claude`, or `cursor` or `codex`), see the failing URL and what the user did, and propose a change. You review the pull request.

## FAQ

### Why does fetch not throw on a 404 or 500?

`fetch()` rejects only when there is no usable response. A 404 or 500 is a response, so the promise resolves and `res.ok` is `false`. Check `res.ok` or `res.status` yourself and throw when it is not what you expect.

### Why does the request work in Postman but fail in the browser?

Postman does not enforce CORS or mixed content rules and does not run browser extensions. The browser does all three. Look for a CORS or Mixed Content line in the console, or `ERR_BLOCKED_BY_CLIENT` in the Network tab.

### Why do I see "Failed to fetch" only for some users?

The usual reasons are on the user's side: an ad blocker, a corporate proxy or VPN, a flaky mobile connection, or an old cached page that calls an API URL you have since changed. Errors that hit everyone point to your server, URL or CORS configuration.

### Can I get the real reason from the error object?

No. The browser hides the reason from the page on purpose, so the `TypeError` message is the same for CORS, DNS and refused connections. The reason is only shown in DevTools. In production, the request URL, the page's origin and whether the user was online are the best clues.

## Sources

- [MDN: Window.fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) (exceptions and HTTP error handling), checked 2 Nov 2026.
- [Chromium fetch_manager.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/fetch/fetch_manager.cc) ("Failed to fetch"), checked 2 Nov 2026.
- [Firefox DOM error messages](https://github.com/mozilla-firefox/firefox/blob/main/dom/bindings/Errors.msg) (`MSG_FETCH_FAILED`), checked 2 Nov 2026.
- [WebKit FetchResponse.cpp](https://github.com/WebKit/WebKit/blob/main/Source/WebCore/Modules/fetch/FetchResponse.cpp) ("Load failed"), checked 2 Nov 2026.
- [MDN: Mixed content](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Mixed_content), checked 2 Nov 2026.

## Related errors

- [CORS: No 'Access-Control-Allow-Origin' header](/guides/cors-blocked-no-access-control-allow-origin/): `has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.`
- [Mixed Content: request blocked](/guides/mixed-content-blocked/): `Mixed Content: The page was loaded over HTTPS, but requested an insecure resource. This request has been blocked`
- [Unexpected token < is not valid JSON](/guides/unexpected-token-not-valid-json/): `SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON`
- [Cannot read properties of undefined](/guides/cannot-read-properties-of-undefined/): `TypeError: Cannot read properties of undefined (reading 'map')`
- [ChunkLoadError: Loading chunk failed](/guides/chunkloaderror-loading-chunk-failed/): `ChunkLoadError: Loading chunk 123 failed.`

All guides: [Fix an error](/guides/).

[Start free](/console/register?utm_source=bugtape_seo&utm_campaign=error-guides). Free plan · No credit card · Set up in minutes. Or [try the live demo](/demo/?from=landing&start=1#capture) first.
