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.com
When 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/data
If 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-22
TCP vs UDP, Explained Through What Breaks When You Pick Wrong
TCP and UDP aren't interchangeable. We walk through the exact failure modes — head-of-line blocking, silent packet loss, Nagle delays — that show up when you pick the wrong transport.
2026-06-22
Write-Ahead Logging: How Databases Survive a Power Cut
How write-ahead logging keeps your data intact when the machine dies mid-write — the log-first rule, fsync, checkpoints, and why PostgreSQL and SQLite both rely on it.
2026-06-22
Backpressure, Explained Through a Queue That Won't Fall Over
What backpressure actually is, why an unbounded queue is a memory leak in disguise, and the four strategies a producer can take when a consumer falls behind.
2026-06-22
What a Bloom Filter Actually Saves You (and When It Lies)
A bloom filter trades a small false-positive rate for big memory savings. Here is the math behind the trade, where it pays off, and the failure mode that bites people.
2026-06-22
Idempotency, Explained Through the Retry That Doesn't Double-Charge
A practical look at idempotency keys: why a retried payment request shouldn't charge a card twice, how the pattern works, and where it quietly breaks in production.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.