Why Isn’t Fetch Working? | Fix Errors That Block Requests

Most fetch failures come from CORS blocks, a bad URL, blocked mixed content, missing credentials, or code that never awaits the response.

You call fetch(), then nothing. No data, no clear clue, just a rejected promise or a console line that says “Failed to fetch.” That message feels vague because it is. It’s a catch-all for a whole stack: your code, the browser’s security rules, the network, and the server response.

This walkthrough gives you a clean path to the root cause. You’ll start with a short set of checks that catch most problems, then move into the bigger buckets: CORS, HTTPS rules, redirects, cookies, request shape, and runtime quirks (browser vs Node).

Start With A 90-Second Reality Check

Before you change code, confirm what’s actually failing. These checks prevent hours of guessing.

  • Check DevTools Network first. If there’s no request line at all, your code path may not run, or a browser extension may block it.
  • Confirm the URL string. A missing https://, a double slash in the wrong spot, or a wrong subdomain will sink the call.
  • Try the same URL in a new tab. If it won’t load there, fetch() won’t rescue it.
  • Look for a CORS error in Console. If you see a CORS message, your server headers (or proxy plan) decide the outcome.
  • Verify request method and headers. A custom header can trigger a preflight request, which then fails before your “real” request even starts.
  • Confirm you’re awaiting the right thing.fetch() resolves on headers, not on JSON parsing, and it won’t reject on HTTP 404/500 by itself.

How Fetch Behaves When It Fails

One detail trips people: fetch() only rejects on network-level failures (DNS, refused connection, CORS blocks, interrupted requests). If the server returns a 404 or 500, the promise still resolves, then response.ok is false.

So you need two layers of handling: the network layer (catch) and the HTTP layer (status checks). The Fetch API behavior is documented in MDN’s Fetch API docs.

A Safer Pattern For Most Apps

This pattern makes failures visible and prevents silent “success” on a bad HTTP status.

async function getJson(url) {
  const res = await fetch(url, { headers: { "Accept": "application/json" } });
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`HTTP ${res.status} ${res.statusText} ${text}`.trim());
  }
  return res.json();
}

If this throws an “HTTP 401” or “HTTP 500,” your request reached the server and came back. That’s a server/auth issue, not a browser block.

Why Isn’t Fetch Working? Common Causes You Can Spot Quickly

When the console shows “TypeError: Failed to fetch,” you’re in the network/security bucket. Use the table below to map the symptom to the next check.

What You See Likely Cause Next Place To Check
“Failed to fetch” + CORS message CORS blocked by browser Response headers; preflight (OPTIONS) in Network
Request missing in Network tab Code path never runs or blocked by extension Breakpoints, call site, try Incognito
(blocked:mixed-content) HTTPS page calling HTTP resource URL scheme; server must be HTTPS
Pending forever, then abort Server stall or no timeout handling Server logs; add AbortController
200 OK, then JSON parse error Response isn’t JSON (HTML error page, empty body) Preview response body in Network
401/403 with “works in Postman” Missing cookies/credentials or CSRF header credentials, same-site cookies, CSRF flow
Only fails on one browser Cached service worker or strict privacy setting Application tab; service worker; storage
Only fails in production Different domain triggers CORS or CSP Deployed headers; CSP; proxy config

Cross-Origin Rules That Stop Requests Cold

CORS is the top cause of “works on server, fails in browser.” If your page is on https://app.example and your API is https://api.example, that’s cross-origin unless you’ve matched scheme, host, and port.

What A CORS Failure Looks Like

You’ll often see a console message about Access-Control-Allow-Origin missing, or a preflight being blocked. A preflight is an OPTIONS request the browser sends before the real request when you use certain methods or headers.

Fix CORS The Right Way

  • Set CORS on the API response. The browser reads server headers, not your JS.
  • Allow only the origins you own. Wildcards can break cookies and can widen access.
  • Handle OPTIONS. Your server must reply to preflight with the right headers and a 200-range status.
  • Watch custom headers. Adding headers like Authorization is normal, but it often triggers preflight, so your server must be ready.

When “no-cors” Seems Tempting

mode: "no-cors" can silence errors, yet it gives you an opaque response you can’t read. That’s fine for a beacon-style request where you don’t need the response body. It’s not a fix for JSON APIs.

HTTPS, Mixed Content, And Certificates

If your site loads over HTTPS, browsers block most HTTP requests from that page. In DevTools, you’ll often see something like mixed content being blocked. Fix it by using an HTTPS endpoint for the API and any assets you request.

Certificate issues can also surface as fetch failures. A cert that’s expired, misconfigured, or not trusted can prevent a connection. If the URL shows a browser warning when opened directly, fetch will fail too.

Requests That “Work” But Return The Wrong Thing

Plenty of fetch “bugs” are just unexpected payloads.

JSON Parse Errors

If you call res.json() and get an error, open the Network entry and look at the response body. A common pattern: the API returns HTML (an error page, a login page, a CDN block page) while your code expects JSON.

Use a content-type guard when the server isn’t consistent.

const res = await fetch(url);
const type = res.headers.get("content-type") || "";
if (type.includes("application/json")) {
  return res.json();
}
const text = await res.text();
throw new Error(`Expected JSON, got: ${type} ${text.slice(0, 120)}`);

Redirects And “Surprise” Endpoints

Redirect chains can lead you to a different origin, which then triggers CORS, or drops cookies, or lands on a login redirect. In Network, check the “Initiator” and the final request URL after redirects.

Cookies, Sessions, And Auth That Break In Browsers

APIs that rely on cookies often fail in fetch calls because cookies aren’t always sent the way people assume. By default, cross-origin requests don’t include cookies unless you opt in.

When You Need Cookies

Use credentials and make sure the server sets cookies that can be sent in your scenario.

// Browser
await fetch("https://api.example.com/me", {
  credentials: "include"
});

If that still fails, check cookie attributes. Cross-site cookie flows often need SameSite=None; Secure. A mismatch shows up in DevTools Issues panel with clear cookie warnings.

When You Use Tokens

If you use Authorization: Bearer ..., confirm you’re passing the header and that your server accepts it from your origin. Also confirm your proxy or CDN doesn’t strip it.

Time-Outs And Hung Requests

fetch() does not time out on its own. If the server stalls, your UI can sit in “loading” forever.

Add A Hard Stop With AbortController

async function fetchWithTimeout(url, ms) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

If abort fixes your UI but the server still hangs, check server timeouts, upstream calls, and load balancer settings.

Node.js And Server Runtimes: Fetch Isn’t Always The Same

In Node, fetch can behave differently from the browser, and older Node versions may not have a global fetch at all. Modern Node includes fetch, backed by Undici, and Node’s docs cover the basics and runtime notes in Node’s Fetch guide.

Common Node-Side Causes

  • Missing global fetch. Your Node version may be too old, so fetch is undefined.
  • TLS differences. A server with a shaky cert chain might still load in your browser yet fail in Node, depending on trust stores.
  • Proxy rules. Production servers often route outbound traffic through a proxy that blocks unknown hosts.
  • DNS in containers. A container can resolve names differently than your laptop.

Second-Pass Triage: Match The Error To The Fix

Once you’ve checked Network and confirmed the request either never leaves, gets blocked, or returns an unexpected payload, use this mapping to tighten the fix.

Error Or Symptom What It Usually Means Fix Direction
TypeError: Failed to fetch Network/CORS/security block Check CORS, mixed content, DNS, extensions
CORS policy blocked access Server didn’t allow your origin Set allow-origin, allow-headers, allow-methods
net::ERR_NAME_NOT_RESOLVED DNS can’t resolve host Fix hostname, DNS, VPN, container DNS
net::ERR_CONNECTION_REFUSED No service listening at host/port Check port, firewall, server process, URL
401/403 in response Auth failed, token missing, cookie not sent Add credentials, refresh token, fix CSRF
SyntaxError in res.json() Body isn’t JSON Inspect response, handle content-type, fix API
Opaque response no-cors mode hides body Use proper CORS instead of no-cors
Request shows “(from ServiceWorker)” Service worker intercepts the call Update SW, clear site data, re-register

Taking A Fetch Call From “Broken” To Trustworthy

If you want fetch to behave predictably across pages, browsers, and deployments, bake in a few habits:

  • Validate inputs at the boundary. Guard the URL, method, and headers before calling fetch.
  • Handle HTTP status on every call. Treat non-2xx as an error path in your app logic.
  • Log the final URL and status. Redirects and proxies can change what you hit.
  • Set a timeout. Users shouldn’t stare at a spinner for minutes.
  • Separate browser and server concerns. Browser fetch is ruled by CORS; server fetch isn’t, yet server fetch has its own proxy/TLS traps.

If you follow the checks in order, you’ll usually land on the cause in a single pass: either the request never fires, it gets blocked by browser rules, or it reaches the server and returns a status/body your code didn’t expect.

References & Sources