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 |
|---|---|
|
The HTTP method: |
|
A |
|
The request body: a string, |
|
Restricts the request: |
|
Whether cookies/HTTP auth accompany the request: |
|
Overrides HTTP caching behavior — |
|
How to handle a redirect — |
|
A relative URL to send as the |
|
An |
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 |
|---|---|
|
The body parsed as a JSON value. |
|
The body as a plain string. |
|
A |
|
An |
|
A |
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:
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:
-
RTCPeerConnectionrepresents 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).