CORS in Plain English: Why the Browser Blocks Your Fetch
A clear walkthrough of CORS and the same-origin policy — what an origin is, why your fetch fails, how servers opt in, and the big misconception about who CORS actually protects.
You wrote a fetch(), the server clearly returned data, and yet the console shows a red CORS error and your code never sees the response. The frustrating part is that nothing is broken — the browser is doing exactly what it was designed to do.
What an origin is, and the rule that governs it
An origin is the combination of three things: scheme + host + port. https://app.example.com and http://app.example.com are different origins (different scheme). So are https://example.com and https://api.example.com (different host), and https://example.com and https://example.com:8443 (different port). All three parts must match for two URLs to share an origin.
The same-origin policy is a foundational browser security rule: JavaScript running on one origin is, by default, not allowed to read responses from a different origin. This isn’t about whether the request can be sent — it’s about whether your script is allowed to look at what comes back.
Why does this matter? Imagine you’re logged into your bank in one tab. You open a sketchy site in another. Without the same-origin policy, that site’s JavaScript could quietly call your bank’s API, ride along on your existing session cookies, and read your account data. The same-origin policy is what stops a random page from reading responses meant for you on other sites.
How a server opts in with CORS
The same-origin policy is strict, but plenty of legitimate cross-origin requests exist — a frontend on app.example.com talking to an API on api.example.com, for instance. Cross-Origin Resource Sharing (CORS) is the mechanism that lets a server say “it’s fine for this other origin to read my responses.”
The server does this with response headers. The key one is Access-Control-Allow-Origin:
Access-Control-Allow-Origin: https://app.example.comWhen the browser sees a matching value (or the wildcard *), it permits your JavaScript to read the response. Without it, the browser blocks the read even though the response physically arrived.
For requests that could have side effects or use unusual headers, the browser sends a preflight first: an automatic OPTIONS request that asks the server “are you going to allow a PUT with a Content-Type: application/json header from this origin?” The server answers with Access-Control-Allow-Methods and Access-Control-Allow-Headers. Only if the preflight passes does the browser send the real request. So-called “simple” requests — basic GET/POST with a short list of allowed headers — skip the preflight.
Cookies add another wrinkle. To send credentials (cookies, HTTP auth) on a cross-origin request, the client sets credentials: 'include' and the server must return Access-Control-Allow-Credentials: true. Critically, when credentials are involved you cannot use Access-Control-Allow-Origin: * — the server has to echo back a specific origin. The wildcard and credentials are mutually exclusive by design.
The misconception: who CORS actually protects
Here is the part that trips up most developers. CORS is enforced by the browser, not the server. And it does not protect your server — it protects your users.
A CORS policy can’t stop anyone from calling your API. curl, a Python script, a backend service, Postman — none of them are browsers, so none of them apply the same-origin policy. They send the request, get the response, and read it freely, completely ignoring whatever Access-Control-Allow-Origin header you set. CORS headers are instructions to the browser about what its own JavaScript is allowed to read.
So if you’re thinking of CORS as an access-control or security layer for your backend, that’s the wrong model. Real server protection comes from authentication, authorization, and rate limiting. CORS exists purely to govern what one website’s scripts can read from another inside a user’s browser.
Debugging a CORS error quickly
The fastest sanity check is to take the browser out of the equation:
curl -i https://api.example.com/dataIf curl returns the data fine but the browser fails, the issue is purely CORS headers — the server works, it just isn’t telling the browser to allow your origin. From there, inspect the response headers (and the preflight OPTIONS response, if there is one) and confirm Access-Control-Allow-Origin matches your exact origin, scheme and port included.
FAQ
Is CORS a way to secure my API?+
Why does my request work in Postman but fail in the browser?+
Why can't I use Access-Control-Allow-Origin: * with cookies?+
Related reading
2026-06-04
ACID vs BASE: What Database Guarantees Actually Promise
ACID and BASE describe two ends of a tradeoff between strict correctness and scalable availability. Learn what each guarantee means, when each fits, and why most modern databases sit somewhere in between.
2026-06-04
Big-Endian vs Little-Endian
Byte order explained: how big-endian and little-endian lay out multi-byte numbers in memory, why network protocols pick one, and when the difference actually bites you.
2026-06-04
Big-O Notation in Plain English
Big-O describes how an algorithm's runtime or memory grows as input grows. Learn the common classes — O(1), O(log n), O(n), O(n log n), O(n^2), O(2^n) — with plain examples.
2026-06-04
Environment Variables and PATH, Explained
What environment variables actually are, why they hold config and secrets, and how PATH decides which binary runs when you type a command.
2026-06-04
Floating Point: Why 0.1 + 0.2 Is Not 0.3
Type 0.1 + 0.2 into almost any language and you get 0.30000000000000004. Here is why IEEE 754 binary floating point does that — and how to handle it correctly.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.