What Happens When

What Happens When You Fetch Data in the Browser

Fetch builds a request, checks browser policy and cache, sends HTTP over the network, then exposes the response as headers and a body stream.

7 min read

A laptop on a dark background with glowing data streams flowing through it, symbolizing web requests and responses.

fetch() starts a browser-managed HTTP request and returns a Promise for a Response. That one line can involve URL parsing, security checks, cache rules, DNS, TLS, redirects, response headers, and body decoding.

const response = await fetch("https://api.example.com/data");
const data = await response.json();

The API is small because the browser handles the network machinery around it.

A Request object is prepared

The browser first builds a Request from the URL and options.

const request = new Request("https://api.example.com/data", {
  method: "GET",
  headers: { Accept: "application/json" },
  credentials: "same-origin",
  cache: "default"
});

At this point the browser knows the method, headers, body, credentials mode, redirect mode, cache mode, and target URL. It has not necessarily sent bytes yet.

Some headers are controlled by the browser and cannot be set by page JavaScript. That protects connection behavior and prevents scripts from spoofing certain request metadata.

Policy checks happen before access

If a script on one origin requests data from another origin, CORS rules decide whether JavaScript can read the response.

For some cross-origin requests, the browser sends a preflight OPTIONS request first. The server must answer with headers such as Access-Control-Allow-Origin and Access-Control-Allow-Methods.

A CORS failure is not the same as a 404. The browser blocks JavaScript from reading the response and fetch() rejects with a network-style error.

The cache may answer first

Before the network is used, the browser checks cache rules.

Depending on the request and cached response headers, it may:

  • reuse a fresh cached response
  • revalidate with ETag or Last-Modified
  • bypass the cache
  • store the new response for later

A revalidation request can return 304 Not Modified, which tells the browser to use the cached body. That saves transfer bytes while still checking freshness.

The network path

If the request needs the network, the browser resolves the host and opens or reuses a connection.

The path can include:

  1. DNS lookup for the domain
  2. TCP or QUIC connection setup
  3. TLS negotiation for HTTPS
  4. HTTP request bytes sent to the server
  5. response headers returned
  6. response body streamed back

HTTP/2 and HTTP/3 can multiplex multiple requests over one connection, so later fetches to the same origin may avoid a new handshake.

The Response object

When response headers arrive, fetch() can resolve with a Response object.

const response = await fetch(url);

console.log(response.status);
console.log(response.ok);
console.log(response.headers.get("content-type"));

Important detail: HTTP error statuses do not reject the Promise.

const response = await fetch("/missing.json");

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

A 404 or 500 is still an HTTP response. Network failures, blocked CORS access, and aborted requests are the cases that reject.

The body is a stream

The response body is a stream of bytes. Helper methods read that stream and convert it:

await response.text();
await response.json();
await response.blob();
await response.arrayBuffer();

response.json() reads the body, decodes text, and parses JSON. If the body is not valid JSON, parsing throws after the response has arrived.

For large responses, you can read chunks from response.body:

const reader = response.body.getReader();
const decoder = new TextDecoder();
let output = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  output += decoder.decode(value, { stream: true });
}

Streaming fits long downloads, generated output, and interfaces that can render partial data.

Redirects and aborts

By default, fetch() follows redirects. You can change that with the redirect option, but most application code accepts the default behavior.

To cancel a request, pass an AbortSignal:

const controller = new AbortController();

const promise = fetch(url, { signal: controller.signal });
controller.abort();

Aborting matters when a user leaves a view, types a new search query, or cancels an upload. The request may already have reached the server, but the browser stops waiting for the result.

Watch it in DevTools

The Network panel shows the parts that fetch() hides:

  • request URL and method
  • status code
  • request and response headers
  • cache status
  • timing breakdown
  • transferred bytes
  • response preview

Use it when a request fails. A CORS error, a 401, a wrong Content-Type, and invalid JSON can look similar in code but show different evidence in DevTools.

fetch() is a small API on top of a large browser pipeline. The reliable pattern is: make the request, check response.ok, inspect headers when needed, then parse the body once.

Read More From Our Blog

Read the blog →

Explore Our Tools

Browse all tools