Networking

This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve.

This section’s bibliography lists the reference material consulted while preparing these pages.

Browsers expose several distinct APIs for talking to a server over the network, and each one exists because it fits a different communication shape. fetch() is a one-shot request/response call — ask for something, get a response back. Server-Sent Events open a long-lived, one-way channel the server pushes data down. WebSockets open a long-lived, two-way channel either side can write to at any time. WebRTC skips the server almost entirely once set up, connecting two browsers' media/data directly to each other. This page covers all four, in that order, moving from the simplest and most common (fetch()) to the most specialized (WebRTC).

fetch()

For a basic HTTP request, fetch() is a three-step, Promise-based process: call fetch() with a URL, get back a Response object as soon as the status/headers arrive, then call a method on that Response to asynchronously obtain the body — so a typical fetch() call chains two `.then()`s (or awaits twice):

fetch("/api/users/current")             // Step 1: make the HTTP GET request
  .then((response) => response.json())  // Step 2: parse the body as JSON
  .then((currentUser) => {              // Step 3: do something with it
    displayUserInfo(currentUser);
  });

See Asynchronous JavaScript for a full worked example of the same request written both with .then() chaining and with async/await — this page focuses on the shape of the fetch() API itself rather than repeating that example.

fetch() replaces the older, callback-based XMLHttpRequest API (also covered on the Asynchronous JavaScript page); there is no reason to reach for XHR in new code.

Response Status, Headers, and When the Promise Rejects

The Promise returned by fetch() resolves to a Response object as soon as the response starts to arrive — typically before the full body has been received. That Response exposes status (the numeric HTTP status code) and statusText (its English description), ok (true for any status from 200 to 299 — the property to check before trusting a response, rather than comparing status to 200 directly), and headers (a Headers object, see below).

Critically, fetch() only rejects its Promise when the request cannot reach the server at all — the user is offline, the host doesn’t resolve, the connection times out. A 404 or a 500 still fulfills the Promise with a Response whose ok is false; nothing throws automatically on a bad status code. A realistic request therefore checks ok explicitly and always ends the chain with a .catch():

fetch("/api/users/current")
  .then((response) => {
    if (response.ok && response.headers.get("Content-Type") === "application/json") {
      return response.json();       // a Promise for the parsed body
    }
    throw new Error(`Unexpected response status ${response.status}`);
  })
  .then((currentUser) => displayUserInfo(currentUser))
  .catch((error) => {
    // Runs for a network failure (fetch() itself rejected) or for the
    // explicit throw above (a bad status or content type).
    console.error("Error while fetching current user:", error);
  });

Headers is iterable and has has()/get() methods for checking or reading individual header values (header names are case-insensitive), so for (const [name, value] of response.headers) { …​ } works directly on it.

Request Options, Headers, and the Request Object

Passing a second argument to fetch() — an options object — controls everything about the outgoing request:

Option What it controls

method

The HTTP method: "GET" (the default), "POST", "PUT", "DELETE", and so on.

headers

A Headers object, a plain object, or an array of [name, value] pairs to send as request headers.

body

The request body: a string, Blob, typed array/ArrayBuffer/DataView, URLSearchParams, or FormData. Not permitted when method is "GET" or "HEAD".

mode

Restricts the request: "cors" (default cross-origin behavior), "same-origin" (fails outright cross-origin), or "no-cors" (limited cross-origin request, response largely hidden from script).

credentials

Whether cookies/HTTP auth accompany the request: "same-origin" (default), "include" (even cross-origin), or "omit".

cache

Overrides HTTP caching behavior — "default", "no-store", "reload", "no-cache", "force-cache".

redirect

How to handle a redirect — "follow" (default), "error" (reject instead), or "manual" (return an opaque Response instead of following it — type: "opaqueredirect", status 0, and headers, including Location, inaccessible from script).

referrer

A relative URL to send as the Referer header, or "" to omit that header entirely.

signal

An AbortSignal (see "Aborting a Request" below) used to cancel the request from outside.

let headers = new Headers({ "Content-Type": "application/json" });

fetch(url, { method: "POST", headers, body: JSON.stringify(requestBody) })
  .then((response) => response.json())
  .then((result) => displayResult(result));

Rather than passing an options object to fetch() directly, the same options can be passed to the Request() constructor (new Request(url, { method, headers, body })), producing a reusable Request object that is then handed to fetch() in place of a URL. fetch() also accepts a URL object as its first argument directly, so query parameters are best built with URL/URLSearchParams (see Utilities) rather than assembled by hand.

Parsing the Response Body

Once a Response has arrived, one of its body methods asynchronously produces the body in the form you need. Each returns a Promise, and each can only be called once per response (the body stream is consumed after the first call):

Method Resolves to

response.json()

The body parsed as a JSON value.

response.text()

The body as a plain string.

response.blob()

A Blob — for large binary payloads (images, video, downloads); the browser may stream it to a temporary file rather than holding it all in memory.

response.arrayBuffer()

An ArrayBuffer, for building a typed array or DataView over binary response data (see Arrays & Typed Arrays).

response.formData()

A FormData object, for a multipart/form-data-encoded response — common as a request body, rare as a response.

Streaming a Response Body

All five methods above wait for the entire body before resolving. When you want to process data as it arrives — to render a progress bar, or to start handling the first rows of a large download before the rest lands — response.body is a ReadableStream. Calling getReader() on it returns a reader whose read() method asynchronously yields one chunk at a time as a Uint8Array, in the same { value, done } shape used by the iterator protocol:

async function streamBody(response, reportProgress) {
  let reader = response.body.getReader();
  let decoder = new TextDecoder("utf-8");
  let body = "";
  while (true) {
    let { done, value } = await reader.read();
    if (value) {
      body += decoder.decode(value, { stream: true });
      reportProgress(value.length);
    }
    if (done) return body;
  }
}

fetch("big.json").then((response) => streamBody(response, updateProgressBar));

bodyUsed on a Response is true once its body has already been consumed by any method (including a prior getReader() read loop), which is worth checking before attempting to read it a second way.

File Uploads and Cross-Origin Requests

A FormData object is the natural request body for uploading files — built either from a <form> element directly, or piece by piece with set()/append(), including File/Blob values pulled from a file <input> or drag-and-drop event: formData.set("avatar", fileInput.files[0]); fetch("/upload", { method: "POST", body: formData }).

A request is same-origin when its URL shares protocol, hostname, and port with the page making it; anything else is cross-origin, and browsers block those by default. Cross-Origin Resource Sharing (CORS) is the escape hatch: the browser automatically attaches an Origin header, and the Promise only proceeds if the server answers with a matching Access-Control-Allow-Origin header — otherwise fetch() rejects. See What is CORS? for the full explanation of how CORS negotiation works and how to validate origins correctly on the backend.

Aborting a Request

AbortController/AbortSignal provide a generic cancellation mechanism fetch() understands: pass a controller’s signal in the request options, and call abort() whenever the request should be cancelled — doing so rejects any Promise associated with that request:

function fetchWithTimeout(url, options = {}) {
  if (options.timeout) {
    let controller = new AbortController();
    options.signal = controller.signal;
    setTimeout(() => controller.abort(), options.timeout);
  }
  return fetch(url, options);
}

Server-Sent Events

HTTP is fundamentally request-driven: the client asks, the server answers. Server-Sent Events (SSE) is the established technique for turning that around so a server can push data to the client without the client asking again every time — the client opens a connection and simply leaves it open; the server writes to it whenever it has something to say, and the browser surfaces each write as an event.

EventSource: Connecting to a Server Endpoint

Creating an EventSource and pointing it at a URL starts (and, if the connection drops, automatically restarts) this long-lived request:

let ticker = new EventSource("/stockprices");

ticker.addEventListener("open", () => console.log("connected"));
ticker.addEventListener("bid", (event) => displayNewBid(event.data));
ticker.addEventListener("error", (event) => console.error("SSE connection error", event));

open fires once the connection is established. error fires on a connection problem; EventSource retries automatically on its own, so an error handler is typically for logging/UI feedback rather than reconnect logic. message fires for any server event that didn’t specify its own name (the default event type) — a named event, like "bid" above, is delivered instead to a listener registered for that specific name.

On the wire, the server just writes plain lines of text terminated by a blank line — event: bid, then one or more data: lines (joined with newlines into event.data if there is more than one), then a blank line marking the end of that event. event: is what routes the event to a specific listener rather than to message.

When Server-Sent Events Are the Right Choice

SSE is a good fit whenever communication is genuinely one-way, server-to-client — live price tickers, activity feeds, progress/notification streams, an inbound stream of chat messages — and the client’s own outbound traffic, if any, is fine as ordinary, occasional HTTP requests rather than needing the same open channel. A chat client is a natural example: an EventSource receives incoming messages, while fetch() posts the user’s own messages as regular one-off requests:

let chat = new EventSource("/chat");
chat.addEventListener("chat", (event) => appendMessage(event.data));

input.addEventListener("change", () => {
  fetch("/chat", { method: "POST", body: `${nick}: ${input.value}` })
    .catch((e) => console.error(e));
  input.value = "";
});

SSE’s built-in auto-reconnect and simple, text-only event model make it considerably less code than a WebSocket for this shape of problem — but it only ever flows in one direction. When the client also needs to push frequent, low-latency messages through the same connection rather than as separate requests, that is the WebSocket’s job, covered next.

WebSockets

Where fetch() is one request/one response and SSE is one open, server-to-client-only channel, a WebSocket is a single persistent connection that either side — client or server — can write to at any time, independently of the other. The connection begins life as an ordinary HTTP request that asks to be "upgraded" to the WebSocket protocol (URLs use wss:// instead of https://), after which it behaves less like HTTP and more like a raw, message-oriented TCP socket.

Creating and Connecting

Creating a WebSocket starts connecting immediately, though the object is not yet connected when the constructor returns:

let socket = new WebSocket("wss://example.com/stockticker");

socket.onopen = () => console.log("connected");
socket.onclose = (event) => console.log("closed", event.code, event.reason);
socket.onerror = (event) => console.error("socket error", event);

readyState tracks progress through four values — CONNECTING, OPEN, CLOSING, CLOSED — and the three events above fire on the matching transitions; calling close() on the socket initiates the graceful shutdown that eventually fires close.

Sending and Receiving Messages

send() transmits a string, Blob, ArrayBuffer, typed array, or DataView; it buffers the message and returns immediately rather than waiting for the network write to complete (bufferedAmount reports how many bytes are still queued). Incoming messages arrive as message events, whose data is a string for text messages or (by default) a Blob for binary ones — set binaryType = "arraybuffer" on the socket to receive binary messages as ArrayBuffer instead:

socket.onmessage = (event) => {
  let update = JSON.parse(event.data);
  applyPriceUpdate(update);
};

socket.send(JSON.stringify({ type: "subscribe", symbol: "GOOG" }));

Unlike some other browser messaging APIs, WebSocket messages are plain strings or byte payloads — there is no structured-clone support for passing rich objects directly, so JSON (or a binary format of your own design) is the usual way to carry structured data over send(). An optional second constructor argument lists application-level sub-protocols the client can speak; the server picks one, exposed afterward as socket.protocol — useful once a service has evolved multiple message-format versions that need agreeing on.

Why WebSockets Suit Real-Time, Bidirectional Applications

Games and chat are the textbook use cases for WebSockets, and for the same underlying reason: both sides need to send frequent, small, low-latency messages whenever they have something to say, not on a fixed request/reply cadence. A multiplayer game pushes position updates from client to server dozens of times a second while the server simultaneously streams other players' state back down; a chat app needs a message typed on one screen to reach every other open screen almost instantly. Re-opening a new fetch() request for every outbound message (and polling, or an EventSource, for inbound ones) adds per-message HTTP overhead and cannot deliver a message from the client to the server without the client initiating it first. A single open WebSocket avoids both costs: one connection, negligible per-message framing, and either side free to write at any moment.

The diagram below contrasts the three models covered so far: `fetch()’s one request per one response, and the WebSocket’s single connection carrying many independent messages in both directions over time:

sequenceDiagram participant Client participant Server rect rgb(230, 230, 230) Note over Client,Server: fetch() -- one request, one response, connection then closes Client->>Server: GET /api/resource Server-->>Client: 200 OK + body end rect rgb(230, 230, 230) Note over Client,Server: WebSocket -- one connection, many messages, either direction, any time Client->>Server: Upgrade: websocket Server-->>Client: 101 Switching Protocols loop connection stays open Client->>Server: send("move", {x, y}) Server-->>Client: message (opponent moved) Server-->>Client: message (chat from another player) Client->>Server: send("chat", "gg") end Client->>Server: close() Server-->>Client: close event end

WebRTC

Unlike the rest of this page, WebRTC is not covered by the book — Flanagan’s 7th edition only points to it as further reading, without dedicated coverage. The material below comes from general/official knowledge instead; verify it against the W3C WebRTC specification before relying on it in production.

WebRTC ("Web Real-Time Communication") is the browser API for peer-to-peer audio, video, and arbitrary data exchanged directly between two browsers, with no server relaying the actual media once a connection is established — the opposite arrangement from fetch(), SSE, and WebSockets, all of which always run through a server.

The Peer Connection, Signaling, and ICE/STUN/TURN

Setting up a direct peer connection still needs a server for one thing: the two browsers have to find each other and exchange enough information to negotiate a connection before any direct link exists. That coordination step is called signaling, and WebRTC deliberately leaves its transport unspecified — an application typically carries it over its own WebSocket or similar channel to a server it controls. A full signaling-server implementation is out of scope here; the roles involved are what matter:

  • RTCPeerConnection represents one browser’s end of the peer-to-peer link. Each side creates one, generates an "offer" or "answer" (a Session Description Protocol, or SDP, blob describing supported media formats and connection parameters), and relays it to the other peer through the signaling server.

  • ICE (Interactive Connectivity Establishment) is the process the two `RTCPeerConnection`s use to discover every viable network path between them — direct local addresses and addresses reachable via NAT traversal — and pick the best one; each discovered address is an "ICE candidate," exchanged via signaling as it’s found.

  • STUN/TURN servers back ICE: a STUN server tells a peer its own public IP/port as seen from outside its NAT, usually enough for a direct path; a TURN server is a fallback relay used only when no direct path can be established, trading the "no server in the media path" property for reachability.

Once ICE settles on a working path, the RTCPeerConnection carries media/data directly between the two browsers without the signaling server’s further involvement.

Media and Data Channels

navigator.mediaDevices.getUserMedia({ audio: true, video: true }) returns a Promise for a MediaStream representing the local camera/microphone; adding its tracks to an RTCPeerConnection (addTrack()) is what turns a signaling handshake into a live audio/video call, with the remote party’s incoming tracks surfacing via the connection’s track event for attaching to a <video> element. Beyond media, RTCPeerConnection can also open an RTCDataChannel for exchanging arbitrary binary or text data peer-to-peer — useful for anything from game state to file transfer — using the same signaling and ICE machinery as the media path.

At an overview level, a typical video call flow is: both peers create an RTCPeerConnection; one creates an offer and sends it through signaling; the other sets it as the remote description and sends back an answer; both exchange ICE candidates as they’re discovered; getUserMedia() supplies local tracks on each side; and once ICE finds a path, media flows directly between the two browsers.

See the official W3C WebRTC specification for the full API surface, including options this overview does not cover (e.g. RTCRtpSender/RTCRtpReceiver for fine-grained media control, renegotiation, and connection statistics).