skip to content

To silence a CORS error, a teammate adds mode: 'no-cors' to a fetch() call and reports that it now resolves without complaint. What does the browser actually hand back in that mode, and when is no-cors a legitimate choice rather than a way of hiding a bug?

level: seniorimportance: should knowfreq 42%

answer

  1. silences the error, not the restriction
  2. status zero, empty headers
  3. made like an img tag would
  4. fine when nothing is read back
  5. quota padding on cached opaque entries

basics

~20 s

no-cors returns an opaque Response: status reads as 0, headers are empty, ok is false, and the body cannot be read. It never grants access to data — it only stops the error. It is legitimate only for fire-and-forget requests or caching a resource you never need to inspect.

solid answer

~50 s

`mode: 'no-cors'` does not obtain permission; it gives up on reading the reply in exchange for not erroring. What resolves is an **opaque** `Response`: `response.type` is `"opaque"`, `status` is `0`, `ok` is `false`, `headers` is empty, and calling `text()` or `json()` yields nothing useful. The request itself is also confined to the safelisted envelope — no custom headers, no methods beyond GET, HEAD and POST — so it is sent much like a form submission. As a data-fetching fix it is worthless: the promise resolving proves nothing, not even that the server returned 200. It is genuinely useful in two places: fire-and-forget calls where the response is irrelevant, and a service worker storing a cross-origin asset it will only ever replay into an `<img>` or `<script>` rather than inspect — with the caveat that opaque entries are padded against the origin's storage quota. Anywhere else, `no-cors` converts a loud, diagnosable failure into a silent one.

go deeper

for a junior

Know that mode: 'no-cors' hides the error but returns nothing you can read — no status, no headers, no body — so it is never how you get data from a blocked API.

for a middle

Describe the opaque Response concretely (type "opaque", status 0, ok false, empty headers) and explain that the request is also restricted to safelisted headers and form-shaped methods.

for a senior

Judge when it earns its place — fire-and-forget writes, or a service worker replaying an asset it never inspects — and name the traps: silent header dropping, indistinguishable errors, and padded storage quota for cached opaque entries.

for a principal

Treat no-cors in a codebase as a signal about observability: it converts a diagnosable configuration failure into a silent one, so set the expectation that any path which reads data must be fixed at the API or origin boundary instead.

## What the mode actually selects The `mode` option on a `fetch` request picks which set of rules the browser applies. `'cors'` is the default for cross-origin calls and is the mode that asks for permission and reports failure loudly. `'same-origin'` refuses cross-origin requests outright. `'navigate'` is reserved for document navigations. `'no-cors'` is the odd one: it asks the browser to make the request the way a plain `<img>` or `<script>` tag would, and to accept that the result is unreadable. The deal is explicit. You give up two things: - **The request is constrained.** Only `GET`, `HEAD` and `POST`; only safelisted headers; only form content types. Setting `Authorization` or `Content-Type: application/json` in `no-cors` mode does not fail loudly — the header is silently dropped, which is its own debugging trap. - **The response is opaque.** Not filtered, not partial — opaque. ## Reading an opaque response ```js const res = await fetch("https://api.other.com/data", { mode: "no-cors" }); res.type; // "opaque" res.status; // 0 res.ok; // false res.headers.get("content-type"); // null — the header list is empty await res.text(); // "" ``` Every field is neutered on purpose. If `status` were visible, a page could probe whether a user is logged into another site by watching 200 versus 302. If headers were visible, it could read `Content-Length` and infer the size of a private document. The browser hands back a placeholder that says only "something came back, or possibly did not" — an opaque response looks identical whether the server returned 200, 404, or 500. That is why the teammate's report is misleading. The error is gone because you asked the browser to stop reporting failures, not because the API granted access. Compare `res.type` against `"cors"` — for a genuinely permitted cross-origin response it is `"cors"`, and for a same-origin one it is `"basic"`. ## The legitimate uses **Fire-and-forget.** You want a request delivered and genuinely do not care about the reply — a ping, a telemetry write, a cache-warming call. There the opacity costs nothing, because there was nothing to read. Be honest that you also lose error reporting: a 500 and a success are indistinguishable, so you must be able to tolerate silent loss. **Caching an asset you will replay, not inspect.** A service worker precaching a font, an image or a script from a CDN that sends no CORS headers can store the opaque response and later serve it into an `<img>`, `<link>` or `<script>`, all of which handle opaque bodies fine. Two caveats belong in the answer: an opaque entry is padded when counted against the origin's storage quota, so a naive precache of many opaque assets can burn far more quota than the bytes suggest; and because status is invisible, a cached 404 error page is indistinguishable from a cached asset and will be replayed forever. The better fix, when the CDN supports it, is to request the asset with proper CORS — `crossorigin` on the tag, ordinary `cors` mode in the worker — so you can check `response.ok` before storing it. ## When it is a bug in disguise Any time you need the data. `no-cors` cannot make an unreadable response readable; the only fixes for that remain the API returning `Access-Control-Allow-Origin` for your origin, proxying the call through your own origin, or making the call server-side. The real damage is diagnostic. A CORS error is a good error: it names the origin, the URL, and the missing header in the console. Replacing it with a resolved promise carrying `status: 0` moves the failure downstream, where it surfaces as a rendering bug or an empty list, and the next engineer has no clue where to start. When you see `mode: 'no-cors'` in a review, the question to ask is: does this code path read anything? If it does, the mode is masking a configuration problem that has not been solved.

  • How can code distinguish a genuinely permitted cross-origin response from an opaque one?
    Check `response.type`: `"basic"` for same-origin, `"cors"` for a cross-origin response the server permitted you to read, and `"opaque"` for a no-cors result. Checking `response.ok` alone is not enough, since an opaque response reports `ok: false` and `status: 0` even when the server returned 200 — the two failures look the same until you inspect the type.
  • What happens to an Authorization header set on a no-cors fetch?
    It is dropped. The mode restricts the request to safelisted headers, and rather than throwing, the browser silently removes anything outside that set before sending. The call therefore goes out unauthenticated, which usually produces a 401 that you also cannot see, since the opaque response hides the status. That combination makes no-cors a poor choice for anything authenticated.
  • Why do opaque responses stored by a service worker consume disproportionate storage quota?
    Because their real size is itself cross-origin information. If quota accounting reflected the exact byte count, a page could store a response and measure the difference to learn how large a private cross-origin resource is. Browsers therefore pad opaque entries by a fixed amount, so a precache of many small opaque assets can exhaust the origin's quota far sooner than the raw bytes suggest.

saying these in an interview costs you the question

  • Thinks no-cors bypasses the same-origin restriction
  • Expects to read JSON from an opaque response
  • Treats a resolved promise as proof the call succeeded
  • Sets custom headers in no-cors and expects them sent
  • Recommends no-cors as the standard fix for a CORS error

context