JavaScript Fetch: Cross-Origin Requests (CORS)
Learn how to make cross-origin requests with the JavaScript Fetch API: how CORS works, the mode and credentials options, and preflight requests.
Cross-origin requests let a web page load data from a server on a different origin than the page itself. They power almost every modern app: calling a public API, talking to your own backend on another subdomain, or embedding a third-party service. This page explains how the browser's CORS rules work, how to make cross-origin calls with the Fetch API, and how to handle credentials, preflight requests, and errors correctly.
What counts as "cross-origin"
An origin is the combination of protocol + host + port. Two URLs share an origin only when all three match. If any one differs, a request between them is cross-origin.
Request from https://app.example.com to… | Same origin? | Why |
|---|---|---|
https://app.example.com/api/users | Yes | identical protocol, host, port |
http://app.example.com/api | No | different protocol (http vs https) |
https://api.example.com/users | No | different host (subdomain counts) |
https://app.example.com:8443/api | No | different port |
Same-origin requests are unrestricted. Cross-origin requests are governed by CORS.
What CORS is (and what it is not)
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that decides whether JavaScript on one origin is allowed to read a response from another origin. The decision is made by the server, which opts in by sending specific HTTP response headers. The browser then enforces them.
Two things are easy to confuse:
- CORS is not something you fix purely in front-end code. If the server doesn't send the right headers, no
fetchoption will make the read succeed. - The request often still reaches the server and runs there; CORS only blocks your script from reading the response. That's why CORS is not a substitute for authentication.
Making a cross-origin request with Fetch
The Fetch API is the modern, promise-based way to make HTTP requests. A plain fetch call to another origin is already cross-origin — the browser handles CORS automatically.
This works because jsonplaceholder.typicode.com returns Access-Control-Allow-Origin: *. If it didn't, the browser would block the read and fetch would reject. See the Fetch API chapter for the full request/response model.
The server headers that make it work
When you make a cross-origin request, the server must include CORS headers to permit it. The ones you'll meet most often:
Access-Control-Allow-Origin— which origin(s) may read the response (an exact origin, or*for any).Access-Control-Allow-Methods— which HTTP methods are allowed (used in preflight responses).Access-Control-Allow-Headers— which request headers the client may send (used in preflight responses).Access-Control-Allow-Credentials— whether cookies/auth may be sent (must betrueto allow credentials).
You configure these on the server, not in fetch. The front end only controls the request.
The mode option
Fetch's mode option declares how cross-origin handling should behave:
'cors'— the default. The request is allowed only if the server returns matching CORS headers; otherwise the read is blocked.'same-origin'— fails immediately for any cross-origin URL.'no-cors'— sends the request but returns an opaque response: you cannot read its status, headers, or body. Useful only for fire-and-forget cases like caching an image in a service worker.
Because 'cors' is already the default, you rarely need to set it, but being explicit documents intent:
On the server, avoid Access-Control-Allow-Origin: * in production. Allow only the specific origins you trust.
Sending credentials
By default, cross-origin fetch requests do not send cookies or HTTP-auth headers. To include them, set the credentials option to 'include'. The server must also respond with Access-Control-Allow-Credentials: true and name your exact origin in Access-Control-Allow-Origin — * is rejected when credentials are involved. See Cookies and document.cookie for how cookies behave across sites.
async function fetchWithCredentials(url) {
try {
const response = await fetch(url, {
mode: 'cors',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch error:', error);
}
}
fetchWithCredentials('https://api.crossorigin.com/secure-data');When a CORS policy blocks a cross-origin read, the browser does not hand your script the response. Instead fetch rejects with a TypeError ("Failed to fetch"), so the error lands in your catch block — not in the if (!response.ok) check.
Preflight requests
For requests the browser considers potentially unsafe, it first sends an automatic OPTIONS request — a preflight — to ask the server whether the real request is allowed. Your code never issues this OPTIONS call; the browser does it for you.
A request triggers a preflight when it is not a "simple request", that is, when it:
- uses a method other than
GET,HEAD, orPOST; - includes custom request headers (for example
AuthorizationorX-Api-Key); - uses a
Content-Typeother thanapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain(sending JSON withapplication/jsonis the most common trigger).
A typical JSON POST therefore costs two round trips — the preflight, then the real request:
async function createPost(url, payload) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // triggers a preflight
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Fetch error:', error);
}
}
createPost('https://api.example.com/posts', { title: 'Hello' });For the preflight to pass, the server must answer the OPTIONS request with Access-Control-Allow-Methods and Access-Control-Allow-Headers that cover what the real request will use. If it fails, the browser raises a network error and your real request is never sent.
Distinguishing the failure modes
CORS errors are notoriously confusing because the browser hides details for security. Use this checklist:
fetchrejects ("Failed to fetch") — usually a CORS block, a network failure, or a failed preflight. Check the browser console for the specific CORS message; it is not exposed to JavaScript.response.okisfalse— the request succeeded and CORS passed, but the server returned a 4xx/5xx status. This is an application error, not a CORS one.- Cookies not sent — you forgot
credentials: 'include', or the server isn't returningAccess-Control-Allow-Credentials: truewith a specific origin.
Wrap calls in try/catch and inspect response.ok separately, as the examples above do.
Best practices
- Lock down
Access-Control-Allow-Origin. List the exact origins you trust instead of*, especially when credentials are involved (*is rejected with credentials). - Always use HTTPS. It protects data in transit and is required for many secure-context APIs.
- Reduce preflights. Avoid unnecessary custom headers, and let the server send
Access-Control-Max-Ageso the browser caches preflight results. - Handle errors gracefully. Separate transport/CORS failures (
catch) from HTTP status errors (!response.ok) and surface useful messages to the user. - Don't rely on CORS for security. It controls what browsers let scripts read; it doesn't authenticate the caller. Validate and authorize every request server-side.
Related chapters
- Fetch API — the foundation for every request on this page.
- Aborting a fetch — cancel slow or unwanted requests.
- Working with JSON — parse and stringify request and response bodies.
- Cookies: document.cookie — how cookies interact with cross-origin credentials.
Summary
A request is cross-origin when its protocol, host, or port differs from the page's. CORS lets the server decide which origins may read the response, and the browser enforces that decision. With Fetch, the 'cors' mode is the default; use the credentials option to send cookies, expect a preflight OPTIONS request for non-simple calls, and handle CORS failures in catch while checking response.ok for HTTP-level errors.