00 Explorable explanation · with Chromium internals
From fetch()
to the wire & back.
A single call crosses the JavaScript engine, a one-way language boundary, three operating-system processes, the security gates, and the socket — then makes the whole trip in reverse. Press play, click any node, and open “under the hood” in any detail for the real Chromium classes and the spec they implement.
Press play to trace the call
Each frame highlights as the request token travels its lane. Amber lanes are owned by your JavaScript; teal lanes are owned by the browser engine.
01 The mental model
JavaScript describes what. The browser decides how.
Everything below is a consequence of one split. fetch() does not send bytes — it hands the browser a description of a request, returns a Promise immediately, and lets the engine do the real work on the far side of a hard boundary. The Web platform reuses this exact shape everywhere:
DOM document.createElement() → JS wrapper → C++ DOM node Events addEventListener() → JS wrapper → C++ dispatch Timers setTimeout() → JS handle → OS timer Fetch fetch() → JS Request → C++ network stack
02 The cast & their roles
A small set of objects — knowing who owns what explains everything.
The crucial asymmetry: the Promise and Request are born synchronously and early; the Response is born late, only after the network and security layers have run.
| Object / layer | Role | Owner | Chromium class |
|---|---|---|---|
| Promise | Handle returned so your code can react later | JS | ScriptPromise / v8::Promise |
| Request | JS-facing description of the outgoing call | JS | blink::Request |
| Headers | Validated, normalized key/value map | JS | blink::Headers |
| internal request | The cross-process request struct | C++ | network::ResourceRequest |
| networking stack | CORS, cookies, cache, sockets | C++ | network::URLLoader → net::URLRequest |
| body stream | Bytes, possibly still arriving | C++ | mojo data pipe → BytesConsumer |
| Response | JS wrapper around status, headers, body | JS | blink::Response |
| transport | TCP text (1.1) or HPACK frames (2) | OS | HttpStreamParser / socket pool |
03 Layers & points of interaction
Every boundary is a one-way copy — not shared memory.
This is precisely why JS cannot forge transport data. Toggle the overlay to dim everything the engine owns and see exactly where your code's reach ends. Click any layer for detail, then expand under the hood.
04 The process model · where the request physically lives
Your fetch leaves the renderer process entirely.
The biggest thing the simple “JS → C++” story hides: a fetch crosses three operating-system processes, connected by mojo IPC pipes. The renderer is sandboxed and owns no sockets. The browser process is the privileged broker. The actual networking — and, critically, the CORS enforcement — happens in a separate network service process. Click any box for the classes involved.
05 Outbound path
From your call stack to bytes on the wire.
fetch("https://api.example.com/users", { method: "POST", headers: { "Authorization": "Bearer token123", "Content-Type": "application/json" }, body: JSON.stringify({ name: "Alex" }) });
Eight frames carry the request out. Each card opens the same deep-dive the hero nodes do — with a Chromium + spec tier inside.
06 CORS & preflight
The browser's veto — fired before a single byte of your request leaves.
Preflight guards against credential leakage, cross-site request forgery, and silent privilege escalation. Because Authorization is non-simple, the engine must ask permission first. Click a node to expand the gate.
fetch() returns its Promise before it even knows whether the request is allowed to exist.
07 Return path
Bytes become a Response, then a Promise resolution, then a value.
Five frames bring the response home. Notice the Response is born before the body finishes downloading — the body is a stream fed across a mojo data pipe.
08 The two-stage await
One promise for the headers, another for the body.
fetch() resolves when the browser can build a Response — usually once response headers arrive. It does not mean the body has downloaded.
const response = await fetch(url); // 1) waits for headers + Response shell const data = await response.json(); // 2) waits for the body stream to drain & parse
The first await gets the metadata shell — status and headers. The second drains and parses the body stream. A slow body, a hung connection, or a manual response.body read all live in stage two; the Response object already exists by then.
09 What JS can read
CORS controls visibility, not just sending.
After a cross-origin response, the engine filters what response.headers will expose. This is also why DevTools ≠ JavaScript: DevTools reads the network-service state; your code reads a filtered wrapper view.
| Tier | Headers | Rule |
|---|---|---|
| always | Cache-Control · Content-Type · Content-Language · Expires · Last-Modified · Pragma | CORS-safelisted, low risk |
| opt-in | anything named in Access-Control-Expose-Headers | server must allow it |
| never | Set-Cookie · Cookie · internal transport headers | credential boundary, stripped in the network service |
10 1.1 vs 2 — same API, different wire
The abstraction boundary hides the framing entirely.
Your JS headers become abstract metadata, then protocol-specific framing chosen by the network service. You never see it. Flip the switch to morph the same headers between text and HPACK frames.
11 Why this architecture exists
Opacity, on purpose.
| Property | What the boundary buys you |
|---|---|
| Security | A sandboxed renderer can't forge Host/Cookie or read credential headers; CORS is enforced out-of-process |
| Consistency | The browser, not your script, enforces the HTTP & Fetch specs |
| Performance | Networking runs native, off the JS thread, in its own process |
| Evolvability | 1.1 → 2 → 3 changes the wire without changing fetch() |