What the error means
fetch() resolves for every HTTP response, including 404 and 500. It rejects with a TypeError only when the browser gets no response it can give to your code.
The browser hides the reason from JavaScript on purpose, so a CORS block, a DNS failure, and a dropped connection all look the same in code. The real reason is in the DevTools Console and Network panel, for example net::ERR_CONNECTION_REFUSED or a CORS message.
| Where | Message |
|---|---|
| Chrome, Edge | TypeError: Failed to fetch |
| Firefox | TypeError: NetworkError when attempting to fetch resource. |
| Safari | TypeError: Load failed |
Common causes
- CORS: the response has no Access-Control-Allow-Origin header for your origin, or the preflight OPTIONS request fails. Error pages from a proxy or gateway often leave these headers out.
- The request sends credentials: "include", but the server answers with Access-Control-Allow-Origin: *. Credentialed requests need the exact origin and Access-Control-Allow-Credentials: true.
- The network failed: the device is offline, DNS lookup failed, or the connection was refused, reset, or rejected for a bad TLS certificate.
- Mixed content: an HTTPS page calls an http:// URL, and the browser blocks it.
- A browser extension or content blocker stopped the request (net::ERR_BLOCKED_BY_CLIENT).
- The user left or reloaded the page while the request was still running.
Reproduce it
In DevTools, open Network, right-click the request, and choose Block request URL. Or set throttling to Offline. Then run the request again.
try { const response = await fetch("https://api.example.com/profile"); console.log(response.status);} catch (error) { // Chrome: TypeError: Failed to fetch // Safari: TypeError: Load failed console.error(error);}Fix it
Tell network failures apart from HTTP errors
Catch the rejection for failures without a response. Check response.ok for error statuses. They need different messages and different retry rules.
TypeScriptasync function getProfile() { let response: Response; try { response = await fetch("/api/profile"); } catch (error) { // No response at all: offline, DNS, CORS, or blocked return { ok: false, reason: "network", error } as const; } if (!response.ok) { return { ok: false, reason: "http", status: response.status } as const; } return { ok: true, profile: await response.json() } as const;}Send the right CORS headers
Fix CORS on the server, not in the browser. For a credentialed request, name the origin, allow credentials, and answer the preflight for the methods and headers you use.
Response headersAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Credentials: trueAccess-Control-Allow-Methods: GET, POSTAccess-Control-Allow-Headers: Content-Type, AuthorizationVary: OriginRetry only safe requests, and not while offline
A GET can run twice without harm; a POST that charges a card cannot. Back off between tries, and stop when the browser reports it is offline.
TypeScriptasync function fetchWithRetry(url: string, tries = 3) { for (let attempt = 1; ; attempt++) { try { return await fetch(url); // GET only: safe to repeat } catch (error) { if (attempt >= tries || !navigator.onLine) throw error; await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 250)); } }}
Catch it in production
The Squasher browser SDK sends this error to your project when it happens, so you do not wait for a user to report it.
- An uncaught fetch rejection is recorded as UnhandledRejection(TypeError), with the page URL, release, environment, and session ID.
- When you catch the error yourself, pass it to captureError with the endpoint and navigator.onLine. Each event then says which call failed and whether the browser was online.
- Repeats of the same error become one issue with an event count, so a jump after a deploy stands out.
import { init, captureError } from "@squasher-ai/browser"; init({ apiKey: import.meta.env.VITE_SQUASHER_API_KEY, projectId: import.meta.env.VITE_SQUASHER_PROJECT_ID, environment: import.meta.env.MODE, release: import.meta.env.VITE_GIT_SHA,}); try { await fetch("/api/profile");} catch (error) { captureError(error as Error, { endpoint: "/api/profile", online: navigator.onLine, });}TypeError: Failed to fetch
TypeError
- Events
- 312
- First seen
- 2d ago
- Last seen
- 4m ago
- Environment
- production
- Service
- checkout-web
Example code and issue.


