Fetch API

You’ve already used fetch for the everyday stuff: pick a method, set some headers, attach a body, read the response. That covers the vast majority of real requests. But fetch accepts a second argument with far more knobs than most people ever touch, and a few of them solve problems that are otherwise miserable to work around.

This chapter walks through the full options object. Treat it as a reference more than a tutorial.

Here’s every option fetch understands, shown with its default value. Alternatives sit in the comments:

let promise = fetch(url, {
  method: "GET", // POST, PUT, DELETE, etc.
  headers: {
    // the Content-Type header is usually set for you,
    // based on what kind of body you pass
    "Content-Type": "text/plain;charset=UTF-8"
  },
  body: undefined, // string, FormData, Blob, BufferSource, or URLSearchParams
  referrer: "about:client", // or "" to send no Referer header,
  // or a url from the current origin
  referrerPolicy: "strict-origin-when-cross-origin", // no-referrer-when-downgrade, no-referrer, origin, same-origin...
  mode: "cors", // same-origin, no-cors
  credentials: "same-origin", // omit, include
  cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached
  redirect: "follow", // manual, error
  integrity: "", // a hash, like "sha256-abcdef1234567890"
  keepalive: false, // true
  signal: undefined, // AbortController to abort request
  window: window // null
});

Long list. The good news is that the defaults are sensible, so an empty options object (or none at all) behaves the way you’d expect for a normal request.

Three of these you already know well. method, headers, and body were covered back in Fetch. The signal option, used to cancel a request in flight, has its own chapter in Fetch: Abort. The rest are what we’ll unpack here.

the request itself
method · headers · body
who’s asking / privacy
referrer · referrerPolicy · credentials
cross-origin rules
mode
network behavior
cache · redirect · keepalive · signal
integrity check
integrity
The fetch options object, grouped by what each field controls.

referrer, referrerPolicy

These two control the HTTP Referer header — the header a browser attaches to tell the server which page the request came from. (The misspelling “Referer” is baked into the HTTP standard and can’t be fixed now.)

You usually don’t think about it. The browser fills it in automatically with the URL of the current page. Most of the time that’s harmless. Sometimes it leaks information you’d rather not send — an internal path, a query string with a token, the exact page a user was on — so it makes sense to trim or drop it.

The referrer option lets you set an exact Referer value (within your own origin) or remove it entirely.

To send no Referer at all, pass an empty string:

fetch('/page', {
  referrer: "" // no Referer header
});

To point it at a specific URL — as long as that URL is on your current origin:

fetch('/page', {
  // assuming the page lives on https://example.com,
  // you can set any Referer you like, but only within this origin
  referrer: "https://example.com/anotherpage"
});

The origin restriction is deliberate. If a page could forge a Referer claiming to come from any site, that header would be worthless as a source signal. So referrer only lets you rewrite it inside your own boundary.

The referrerPolicy option is the broader tool. Instead of naming one exact value, you hand the browser a rule and it decides what to send for each request based on where the request is going.

To reason about the rules, split requests into three cases:

1. same origin
example.com → example.com
2. another origin
example.com → other.com
3. HTTPS → HTTP
secure → less secure
The three request situations a referrer policy has to answer for.

That third case, dropping from HTTPS to plain HTTP, is treated with extra suspicion because a secure page shouldn’t spill its URL onto an unencrypted connection. The policy values come from the Referrer Policy specification:

  • "strict-origin-when-cross-origin" — the default. Same-origin gets the full Referer; cross-origin gets only the origin; an HTTPS→HTTP request gets nothing.
  • "no-referrer-when-downgrade" — send the full Referer every time, except on an HTTPS→HTTP downgrade, where nothing goes.
  • "no-referrer" — never send Referer, in any case.
  • "origin" — always send just the origin, never the path. So http://site.com instead of http://site.com/path.
  • "origin-when-cross-origin" — full Referer for same-origin, origin-only for cross-origin.
  • "same-origin" — full Referer for same-origin, nothing for cross-origin.
  • "strict-origin" — send the origin only, and send nothing on an HTTPS→HTTP downgrade.
  • "unsafe-url" — always send the full URL, even on a downgrade. The name is a warning: this leaks the most.

The whole matrix in one table:

Value To same origin To another origin HTTPS→HTTP
"no-referrer" - - -
"no-referrer-when-downgrade" full full -
"origin" origin origin origin
"origin-when-cross-origin" full origin origin
"same-origin" full - -
"strict-origin" origin origin -
"strict-origin-when-cross-origin" or "" (default) full origin -
"unsafe-url" full full full

A concrete case. Say your site has an admin area whose URL layout you’d rather not advertise. By default, a fetch to an outside server carries the full page URL in Referer:

Referer: https://example.com/admin/secret/paths

If you want outside sites to see only the origin, not the path, set the policy:

fetch('https://another.com/page', {
  // ...
  referrerPolicy: "origin-when-cross-origin" // Referer: https://example.com
});

You could apply this to every fetch in your app — for instance, by routing all requests through one wrapper function that sets it. Compared to the default, the only change is that cross-origin requests now send just the origin (https://example.com, no path). Same-origin requests still carry the full Referer, which can help when you’re reading your own server logs for debugging.

Rather than read the matrix top to bottom, try it. Pick a policy and a destination and watch exactly what Referer the browser would attach. The page you’re “on” is https://example.com/admin/reports?tab=3.

interactiveReferrer policy resolver

mode

The mode option is a guard rail against accidental cross-origin requests:

  • "cors" — the default. Cross-origin requests are allowed, following the CORS rules from Fetch: Cross-Origin Requests.
  • "same-origin" — cross-origin requests are forbidden outright.
  • "no-cors" — only “simple” cross-origin requests are allowed, and you can’t read the response body from JavaScript.

Where this earns its keep: the URL you’re fetching comes from somewhere you don’t fully trust — a third-party config, user input, a value from another service. Setting mode: "same-origin" gives you a hard “off switch” so that even if the URL points somewhere unexpected, the request can’t leave your origin.

credentials

The credentials option decides whether fetch sends cookies and HTTP Authorization headers along with the request.

  • "same-origin" — the default. Send them for same-origin requests, withhold them cross-origin.
  • "include" — always send them. For a cross-origin response to be readable in JavaScript, the server must reply with Access-Control-Allow-Credentials, as covered in Fetch: Cross-Origin Requests.
  • "omit" — never send them, not even for same-origin requests.
value
same-origin
cross-origin
“same-origin” (default)
send
withhold
“include”
send
send*
“omit”
withhold
withhold
* server must return Access-Control-Allow-Credentials for JS to read the response
How each credentials value behaves for same-origin vs. cross-origin requests.

cache

Out of the box, fetch plays by the same caching rules as any browser request. It honors Expires and Cache-Control, sends conditional headers like If-Modified-Since, and reuses cached responses when the headers say it may. Whether something is actually cached depends on those HTTP headers, not on fetch itself.

The cache option lets you step around that machinery or tune how it’s used:

  • "default" — standard HTTP cache behavior, respecting the usual headers.
  • "no-store" — ignore the HTTP cache completely: don’t read from it, don’t write to it. This also becomes the effective default if your request already carries one of If-Modified-Since, If-None-Match, If-Unmodified-Since, If-Match, or If-Range.
  • "reload" — don’t read from the cache, but do store the fresh response (if its headers allow caching). Handy for a “hard refresh” of one resource.
  • "no-cache" — if there’s a cached copy, send a conditional request to check whether it’s still valid; otherwise send a normal request. Either way, store the response.
  • "force-cache" — use the cached response even if it’s stale. Only fall back to a network request when nothing is cached.
  • "only-if-cached" — use the cached response even if stale, and if there’s nothing cached, fail with an error instead of hitting the network. This one only works with mode: "same-origin".
mode
reads cache?
writes cache?
“default”
if fresh
yes
“no-store”
no
no
“reload”
no
yes
“no-cache”
revalidate
yes
“force-cache”
even if stale
on miss
“only-if-cached”
even if stale
error on miss
Reading from cache vs. writing to cache, per mode.

redirect

When a server answers with a redirect (a 301, 302, and so on), fetch follows it for you by default. You get the final response, and the hop is invisible.

The redirect option changes that:

  • "follow" — the default. Follow redirects automatically.
  • "error" — treat a redirect as a failure and reject the promise.
  • "manual" — don’t follow, but don’t error either. You get a special response with response.type === "opaqueredirect", a zeroed status, and most properties empty. This lets you detect that a redirect happened and decide what to do, though the response body and target are deliberately hidden from you.
server replies 302
“follow”go to new URL, resolve with final response
“error”promise rejects
“manual”opaqueredirect response, empty status
Same 302 from the server, three different outcomes depending on redirect.

integrity

The integrity option asks fetch to verify that a response matches a checksum you already know. If the bytes don’t hash to what you specified, the request fails. It’s a defense against tampering or a corrupted download.

Per the Subresource Integrity specification, the supported hash functions are SHA-256, SHA-384, and SHA-512. A browser may support more.

Suppose you’re downloading a pinned library build and you know its SHA-256 checksum is "9c8d2f" (a real one is much longer). You attach it like this:

fetch('https://cdn.example.com/charting-lib.js', {
  integrity: 'sha256-9c8d2f'
});

fetch computes the SHA-256 of what actually arrives and compares. Match, and you get the response as usual. Mismatch, and the promise rejects — you never see the suspect bytes.

Here’s the mechanism in miniature. Below, a file arrives and gets hashed; the result is compared against a checksum pinned up front. Change even one character of the “downloaded” bytes and the hash no longer matches — exactly the case where a real fetch would reject and hand you nothing. (The tiny hash here is a stand-in so you can read it; a real request uses SHA-256.)

interactiveIntegrity check: hash and compare

keepalive

The keepalive option lets a request outlive the page that started it.

The classic use case is analytics on page exit. Say you’re collecting how a visitor used the page — clicks, which sections they scrolled to — and you want to send that summary to your server the moment they leave. You’d naturally hook into the unload:

window.onunload = function() {
  fetch('/analytics', {
    method: 'POST',
    body: "session-summary",
    keepalive: true
  });
};

Normally this fails. When a document unloads, the browser tears down its pending network requests, and a fetch fired during unload would be cut off before it left. keepalive: true tells the browser to carry the request through in the background even after the page is gone. Without it, the request in that example wouldn’t reliably reach the server.

keepalive: false
page unloads
request aborted
keepalive: true
page unloads
browser finishes it in background
reaches server
Without keepalive the request dies with the page; with it, the browser completes it in the background.

That power comes with two constraints:

  • Bodies are capped at 64KB. You can’t ship megabytes on the way out.
    • If you’re gathering a lot of data over the session, send it in batches as you go, so there’s little left to flush in the final onunload request.
    • The 64KB limit is shared across all in-flight keepalive requests at once. You can run several in parallel, but their body sizes have to add up to 64KB or less.
  • You can’t use the response once the page is gone. In the example, the fetch succeeds thanks to keepalive, but any .then you attach to handle the reply won’t run — the JavaScript environment it belonged to no longer exists.
    • For fire-and-forget cases like analytics, that’s fine. The server takes the data and typically returns an empty response nobody needs to read.