TypeError: Failed to Fetch: Causes, Diagnosis, and Complete Fix Guide

Quick answer

💡TypeError: Failed to fetch is thrown by the browser Fetch API when a network request fails before receiving any HTTP response. Common causes include CORS preflight rejection, mixed content (an HTTP URL requested from an HTTPS page), the network being offline, or an invalid URL. Unlike 4xx and 5xx errors, this error means the browser never received a response at all. Check DevTools Network tab for red failed entries and inspect the CORS preflight OPTIONS request.

Error symptoms

  • TypeError: Failed to fetch thrown from a fetch() call in the browser
  • TypeError: fetch failed thrown in Node.js 18+ using the built-in fetch API
  • net::ERR_FAILED or net::ERR_CONNECTION_REFUSED visible in Chrome DevTools Network tab
  • DevTools Network tab shows a red entry with type preflight that has no response
  • navigator.onLine reports true but fetch calls still fail with TypeError
  • CORS error in the browser console followed by a TypeError: Failed to fetch in the catch block

Common causes

  • CORS preflight OPTIONS request rejected by the server due to missing Access-Control-Allow-Origin header
  • Mixed content: calling an HTTP URL via fetch() from a page served over HTTPS
  • Network connectivity lost after the page loaded but before the fetch call executed
  • AbortController.signal triggered before the request completed, causing AbortError not TypeError
  • Node.js version older than 18 where built-in fetch is unavailable requires node-fetch or undici
  • Invalid URL passed to fetch() such as a relative path that resolves incorrectly in Node.js

When it happens

  • Calling a third-party API from a browser without the server setting CORS headers
  • A mobile user's connection drops between page load and a user-initiated API call
  • Developing locally over HTTP and fetching resources from an HTTPS API that blocks mixed content
  • Deploying a frontend that previously ran on HTTP to HTTPS without updating API base URLs
  • Running server-side fetch in Node.js 16 or earlier where global fetch is not available

Examples and fixes

A bare fetch call hides the actual error cause. Adding structured error handling exposes the failure mode.

Diagnosing and handling fetch failures with full error visibility

❌ Wrong

async function loadUserData(userId) {
  try {
    const res = await fetch(`/api/users/${userId}`);
    const data = await res.json();
    return data;
  } catch (err) {
    // err.message is always 'Failed to fetch'
    // No visibility into CORS vs offline vs bad URL
    console.error('Request failed');
    return null;
  }
}

✅ Fixed

async function loadUserData(userId) {
  try {
    const res = await fetch(`/api/users/${userId}`);
    if (!res.ok) {
      const body = await res.text();
      throw new Error(`HTTP ${res.status}: ${body.slice(0, 200)}`);
    }
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      console.warn('Request was aborted');
      return null;
    }
    if (!navigator.onLine) {
      console.error('Network offline');
      return null;
    }
    // TypeError: Failed to fetch — likely CORS or invalid URL
    console.error('Fetch error:', err.message, 'URL:', `/api/users/${userId}`);
    throw err;
  }
}

The original code catches all errors as the same opaque message 'Request failed', making it impossible to distinguish between a 404 response, a CORS rejection, a network outage, or an AbortController cancellation. The fixed version first checks res.ok to detect HTTP error responses (which do not throw), then distinguishes AbortError from network failures in the catch block. Logging the URL alongside the error message is essential because the same function may be called with different IDs and only some URLs may trigger the failure. Checking navigator.onLine provides a quick hint about offline status, though it does not catch all network failures.

Add a request timeout that cancels the fetch after a deadline and retries with exponential backoff.

Implementing fetch with timeout using AbortController

❌ Wrong

// No timeout: hangs indefinitely on slow or unresponsive servers
async function callApi(url, data) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  return res.json();
}

// Caller has no way to cancel or retry
const result = await callApi('/api/process', payload);

✅ Fixed

async function callApi(url, data, timeoutMs = 10000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
      signal: controller.signal
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } finally {
    clearTimeout(timer);
  }
}

// Retry with exponential backoff
async function callApiWithRetry(url, data, maxAttempts = 3) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      return await callApi(url, data);
    } catch (err) {
      if (i === maxAttempts - 1) throw err;
      await new Promise(r => setTimeout(r, 200 * Math.pow(2, i)));
    }
  }
}

A fetch() call without a timeout can hang indefinitely when a server accepts the TCP connection but never sends a response. AbortController provides the cancellation mechanism: calling controller.abort() causes fetch to throw an AbortError immediately. Wrapping the timeout in a finally block ensures clearTimeout always runs even if the fetch throws, preventing timer leaks. The retry loop uses exponential backoff — 200ms, 400ms, 800ms — which reduces pressure on struggling services. Note that AbortError should not trigger a retry since it indicates the caller explicitly cancelled the request. Only retry on TypeError (network failure) or specific 5xx status codes that indicate transient server issues.

Network layer vs HTTP layer failures

TypeError: Failed to fetch occupies a specific point in the request lifecycle: it occurs when the browser cannot complete the network transaction and receive any HTTP response. This distinguishes it sharply from HTTP error responses like 404 Not Found or 500 Internal Server Error, which arrive as HTTP responses and do not cause fetch to throw. A TypeError means the browser gave up before the server ever sent back a status code.

CORS, or Cross-Origin Resource Sharing, is the most common cause of this error in browser-based applications. When a browser makes a cross-origin fetch — a request to a different domain, port, or protocol than the page's own origin — it first sends a preflight OPTIONS request to ask the server which cross-origin requests are allowed. If the server does not respond to the OPTIONS request with appropriate Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers headers, the browser blocks the actual request entirely. From JavaScript, this appears as TypeError: Failed to fetch. The actual CORS error message appears in the browser console, but the JavaScript catch block only receives the TypeError.

Mixed content is a distinct cause that is frequently confused with CORS. When an HTTPS page fetches a resource from an HTTP URL, the browser blocks the request unconditionally regardless of CORS headers. This is a security policy that prevents HTTPS pages from loading potentially tampered content over unencrypted connections. The block happens at the network layer, producing TypeError: Failed to fetch, and the browser console shows a Mixed Content error. Switching the fetched URL from http:// to https:// resolves this, assuming the target server supports HTTPS.

In Node.js, the built-in fetch API became available in Node.js 18. Code that runs fetch() in Node.js 16 or earlier will throw TypeError: fetch is not a function, which is a different error than Failed to fetch. When using the built-in fetch in Node.js 18+, network failures produce TypeError: fetch failed with a cause property that contains the underlying error. Inspect err.cause to get the specific ECONNREFUSED, ETIMEDOUT, or ENOTFOUND error that explains why the connection failed.

Invalid URLs cause fetch to throw TypeError synchronously before any network request is made. A URL missing the protocol scheme, a relative URL passed to fetch in Node.js where there is no base URL, or a URL with invalid characters all cause an immediate throw. Check that the URL string is fully qualified with a scheme like https:// and does not contain unexpected undefined values substituted from template literals.

Reading DevTools Network tab for fetch failures

Chrome DevTools Network tab is the primary diagnostic tool for TypeError: Failed to fetch in browser applications. Open DevTools with F12, navigate to the Network tab, trigger the failing request, and look for red entries. A red entry marked failed indicates the browser could not complete the request. Click the entry to see the Request Headers, Response Headers, and Timing panels.

For CORS failures, look for two network entries: a preflight OPTIONS request and the actual request. If the OPTIONS request appears red, inspect its Response Headers for the Access-Control-Allow-Origin header. If the header is absent, the server is not configured to allow cross-origin requests from your origin. If only the actual request appears red and no OPTIONS request was made, the request may qualify as a simple request that did not trigger a preflight — check the browser console for the specific CORS error message.

The Timing panel in DevTools shows how far through the request lifecycle the browser got before failure. If the Stalled time is large and the connection shows no Waiting time, the browser could not even establish a TCP connection — suspect the server being down, a firewall blocking the port, or an incorrect hostname. If Stalled is small but the request still fails, the server accepted the connection but rejected it at the CORS preflight stage.

For Chrome specifically, the error code in the Network tab is valuable. ERR_CONNECTION_REFUSED means the server actively rejected the connection — the port is closed or nothing is listening. ERR_NAME_NOT_RESOLVED means DNS lookup failed — the hostname does not resolve. ERR_SSL_PROTOCOL_ERROR means a TLS handshake error. ERR_NETWORK_CHANGED means the network interface changed during the request, common on mobile devices switching between WiFi and cellular. Each of these requires a different fix.

For Node.js, add console.error(err.cause) to your catch block when using Node.js 18+ built-in fetch. The cause property contains the underlying network error from the operating system with a code property (ECONNREFUSED, ETIMEDOUT, ENOTFOUND) and an address property showing the IP address the connection was attempted to. This information is much more actionable than the top-level TypeError message alone. Test the same request with curl using the same URL and headers to verify whether the issue is in the Node.js fetch implementation or in the server.

Fixing CORS, mixed content, and network errors

CORS fixes must be applied on the server, not in the browser. The browser enforces CORS as a security policy and cannot be overridden by client-side code. The server must return the Access-Control-Allow-Origin header with either the specific origin (https://yourapp.com) or the wildcard * for public APIs. For requests that include credentials — cookies or HTTP authentication — the server must return Access-Control-Allow-Credentials: true and must specify the exact origin rather than a wildcard.

For preflight failures, ensure the server handles OPTIONS requests and returns the appropriate headers. In Express.js, use the cors middleware: app.use(cors({ origin: 'https://yourapp.com', credentials: true })). In other frameworks, configure the allowed methods, headers, and origins in the CORS configuration. The Access-Control-Allow-Methods header must include the actual HTTP method being used (GET, POST, PUT, etc.), and Access-Control-Allow-Headers must include any custom headers like Authorization or Content-Type.

Mixed content errors require updating the fetched URL to use HTTPS. If the target API does not support HTTPS, consider proxying the request through your own server, which can make the HTTP request from a secure server-to-server context and return the result to the browser over HTTPS. This eliminates mixed content blocking while keeping the communication between servers encrypted.

For development environments, a common pattern is to use a proxy configured in the development server. Create-React-App supports a proxy field in package.json. Vite supports a proxy configuration in vite.config.js. These development proxies forward requests from the browser to a different origin, bypassing CORS by making the request appear same-origin to the browser. This is a development-only solution and must be replaced with proper server-side CORS configuration for production.

For cookies across origins, set credentials: 'include' in the fetch options: fetch(url, { credentials: 'include' }). The server must respond with Access-Control-Allow-Credentials: true and must use a specific origin rather than wildcard * in Access-Control-Allow-Origin. Without both of these, the browser will not send cookies and the server will not accept them. Build and test requests in /tools/http-request-builder to verify headers are configured correctly before writing code.

Uncommon fetch failure scenarios

The response.ok property is false for 4xx and 5xx responses, but fetch does not throw a TypeError for these. The error you receive in catch is only thrown when the network request itself fails. A common mistake is checking only for thrown errors and ignoring response.ok, resulting in code that silently passes error responses to res.json() and either fails with a JSON parse error when the error body is HTML, or returns an incorrect error object. Always check res.ok after every fetch call and throw explicitly based on it.

AbortController.abort() causes fetch to throw an AbortError, which is distinct from TypeError. The error name is 'AbortError' and the message is 'The user aborted a request.' AbortError should not trigger a retry because the abort was intentional. Check err.name === 'AbortError' before applying retry logic. A common bug is creating a new AbortController and calling abort() on it immediately after creating it, which aborts the request before it starts — check that abort is only called after a meaningful timeout or explicit user action.

navigator.onLine can report true even when actual network requests fail. The browser sets navigator.onLine to false only when the device has no network interface at all. A device connected to a WiFi network that has no internet access, or connected to a VPN that blocks certain routes, will show navigator.onLine === true while all fetch calls fail with TypeError. Use navigator.onLine as a quick hint, not as a definitive network check. The only reliable way to know whether a specific server is reachable is to make a request to it.

Service workers can intercept fetch requests and produce their own responses. If a service worker is registered and its fetch event handler throws or calls event.respondWith with a rejected promise, the browser delivers a TypeError: Failed to fetch to the original caller even if the server would have responded successfully. When debugging fetch failures in applications with service workers, temporarily disable the service worker in DevTools (Application tab, uncheck 'Bypass for network') to isolate whether the service worker is responsible.

Request body size limits on reverse proxies or load balancers cause connection resets that appear as TypeError: Failed to fetch. Nginx's client_max_body_size defaults to 1MB. AWS API Gateway has a 10MB payload limit. Cloudflare has a 100MB limit on its free tier. When uploading large files via fetch, check that all reverse proxies in the request path are configured to allow the expected body size. A connection reset mid-transfer appears as a TypeError because no HTTP response was received.

Mistakes developers make when handling fetch errors

Assuming that every error from fetch is a network failure is the most common mistake. fetch throws TypeError for network failures, but it also throws TypeError for invalid URLs and SyntaxError for malformed JSON in res.json(). Code that catches all errors and treats them as network failures will misdiagnose URL bugs and JSON parsing errors as connectivity problems. Check the error name and message to distinguish the specific failure type before logging or displaying an error message.

Retrying on all TypeError exceptions without checking whether the failure is transient will create retry storms against servers that are intentionally rejecting requests. CORS rejections, mixed content blocks, and invalid URL errors will not resolve on retry. Only retry on errors that are likely transient: timeouts, connection refused for services that may be restarting, and network change errors on mobile. Add a retry count limit and exponential backoff to prevent tight retry loops from overwhelming a struggling service.

Sending fetch requests with credentials: 'include' without configuring the server to accept credentialed requests is a frequent CORS misconfiguration. When credentials are included, the server must return Access-Control-Allow-Credentials: true and must specify the exact origin rather than * in Access-Control-Allow-Origin. Browsers enforce both requirements strictly. A server that returns Access-Control-Allow-Origin: * with a credentialed request causes a CORS failure even though the wildcard seems permissive.

Ignoring the response body when handling error responses loses important diagnostic information. When a server returns a 4xx or 5xx response, the response body often contains a detailed error message, validation failure details, or a rate limit explanation. Code that only checks res.status and discards the body makes debugging production incidents much harder. Always await res.text() or res.json() for error responses and include the content in logs or error messages.

Failing to handle the case where fetch is not defined in older Node.js environments causes TypeError: fetch is not a function, which is a different error than the network failure TypeError. Add a runtime check or use the node-fetch package for Node.js 16 and earlier. In Node.js 18+, fetch is available globally without any import. In Node.js 20+, fetch is stable and recommended. Verify your Node.js version at the start of any fetch troubleshooting session to rule out the missing-fetch case.

Building reliable fetch-based API clients

Wrap all fetch calls in a centralized API client function rather than calling fetch directly throughout your codebase. The client function sets consistent defaults for timeout, headers, base URL, and error handling. All callers get the same behavior without duplicating error handling logic. When you need to change the timeout globally or add an authentication header, you update one place. This pattern also makes it straightforward to add request interceptors for logging, authentication token refresh, or telemetry.

Always pass an AbortController signal to long-running fetch calls. Create a controller with a timeout using setTimeout(() => controller.abort(), N). Cancel the timeout in a finally block if the request completes normally. Components that unmount before a fetch completes should call abort() on their component-level controller to prevent setState calls on unmounted components in React applications and to avoid processing stale responses that arrived after navigation.

Set explicit Content-Type headers when sending JSON request bodies. Without Content-Type: application/json, some servers may reject the request or attempt to parse the body as form data. Set Accept: application/json to communicate to the server that JSON is the expected response format. Some APIs respond with different content types based on Accept headers, and specifying JSON ensures consistent deserialization. Build and verify your request configuration in /tools/http-request-builder before implementing it in code.

Implement structured logging for all fetch failures that includes the request URL, method, error message, response status if available, and a request ID that correlates with server logs. Anonymous TypeError: Failed to fetch entries in production logs are nearly impossible to debug. A log entry with the URL, timestamp, and error code allows you to find the corresponding server log entry if one exists, or to identify patterns like a particular endpoint or user segment experiencing failures.

Test your CORS configuration with a real browser rather than curl. curl does not enforce CORS and will succeed on requests that browsers reject. Use the /tools/http-request-builder or a browser extension that makes cross-origin requests to verify that the server returns correct CORS headers before deploying. Automate CORS verification in your CI pipeline with a headless browser test that makes cross-origin requests from an HTTP server running locally on a different port, confirming that the CORS headers are present and correct in every deployment.

Quick fix checklist

  • Open DevTools Network tab and check for red preflight OPTIONS request entries
  • Verify the server returns Access-Control-Allow-Origin header matching your origin
  • Check that all API URLs use https:// when the page is served over HTTPS
  • Add credentials: 'include' and Access-Control-Allow-Credentials: true for cookie requests
  • Inspect err.cause in Node.js 18+ for the specific system error code
  • Test the same request with curl to isolate whether CORS or server availability is the issue
  • Add AbortController with setTimeout to prevent indefinite hangs on unresponsive servers
  • Check response.ok after every fetch call and handle HTTP error responses explicitly

Related guides

Frequently asked questions

What does TypeError: Failed to fetch mean exactly?

TypeError: Failed to fetch means the browser's Fetch API could not complete the network request and receive any HTTP response. The error occurs before any HTTP status code is received. Common causes include CORS preflight rejection, mixed content blocking (HTTP URL from HTTPS page), network offline, DNS failure, or TCP connection refused. Unlike 4xx and 5xx responses, this error means the server never sent back a response at all.

How do I tell if the failure is CORS or network offline?

Open DevTools Network tab and look for a preflight OPTIONS request that appears red or failed. A CORS failure shows a red preflight entry with no Access-Control-Allow-Origin header in the response. A network offline failure shows ERR_NETWORK_CHANGED or ERR_INTERNET_DISCONNECTED. Check navigator.onLine as a quick hint. Both produce TypeError: Failed to fetch in the catch block, but the DevTools Network tab clearly distinguishes the two causes.

Why does fetch succeed in curl but fail in the browser?

curl does not enforce CORS, mixed content policies, or browser security restrictions. A request that succeeds in curl but fails in the browser is almost always a CORS issue. The server is accessible and responding, but it is not returning the Access-Control-Allow-Origin header required for cross-origin browser requests. Fix CORS on the server side — add the appropriate headers for your allowed origins. Client-side code cannot bypass CORS without a server-side proxy.

Does AbortController.abort() cause TypeError: Failed to fetch?

No. AbortController.abort() causes fetch to throw an AbortError, not TypeError. The error has name 'AbortError' and message 'The user aborted a request.' In your catch block, check err.name === 'AbortError' to distinguish intentional cancellations from network failures. Do not retry on AbortError because the cancellation was intentional. TypeError indicates an unintended network failure that may warrant retry with backoff.

How do I fix CORS for a cross-origin fetch request?

CORS must be fixed on the server. Add Access-Control-Allow-Origin: https://yourapp.com to all responses from your API server. For preflight requests, handle OPTIONS requests and return Access-Control-Allow-Methods and Access-Control-Allow-Headers headers. For credentialed requests with cookies, add Access-Control-Allow-Credentials: true and use the specific origin rather than the wildcard *. In Express, use the cors middleware package to configure this correctly.

What is mixed content and why does it cause fetch to fail?

Mixed content occurs when an HTTPS page fetches a resource from an HTTP URL. Browsers block this unconditionally to prevent HTTPS pages from loading content that could be tampered with over an unencrypted connection. The fix is to change the fetched URL from http:// to https://, assuming the target server supports HTTPS. If not, proxy the request through your own server so the browser-to-server leg is always HTTPS.

Is fetch available in Node.js without installing packages?

Yes, starting in Node.js 18 as experimental and stable in Node.js 20. Node.js 16 and earlier do not have a built-in fetch. On older versions, install node-fetch with npm install node-fetch and import it explicitly: import fetch from 'node-fetch'. In Node.js 18+ with the built-in fetch, network errors include an err.cause property with the underlying system error code (ECONNREFUSED, ETIMEDOUT, ENOTFOUND) which is more informative than the browser TypeError.

How do I add a request timeout to fetch?

Use AbortController with setTimeout: create a controller, set a timer that calls controller.abort() after your timeout, pass controller.signal to fetch options, and clear the timer in a finally block. For example: const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 10000); try { const res = await fetch(url, { signal: controller.signal }); ... } finally { clearTimeout(timer); }. This pattern cancels the request after 10 seconds and prevents timer leaks.

Should I retry after TypeError: Failed to fetch?

Retry only for transient network failures, not for all TypeError causes. Safe to retry: ECONNREFUSED when a server may be restarting, timeout/AbortError from a flaky connection (but use backoff). Do not retry: CORS rejections, mixed content blocks, or invalid URL errors, as these will fail identically on every attempt. Implement exponential backoff starting at 200ms and limit to 3 attempts. Always check err.name and err.cause before deciding to retry.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.