Skip to content

Browser Data, Networking, and State ​

Browser applications exchange data under constraints that command-line programs rarely face: origins, navigation, unreliable networks, user-controlled storage, and a UI that must remain coherent while work is pending.

fetch resolves when an HTTP response arrives ​

js
const response = await fetch("/api/transactions");

The promise rejects for network-level failure or cancellation. An HTTP 404 or 500 is still a successfully received response, so check ok or status:

js
async function loadTransactions({ signal } = {}) {
  const response = await fetch("/api/transactions", {
    headers: { Accept: "application/json" },
    signal,
  });

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

  const value = await response.json();
  return parseTransactions(value);
}

response.json() is also asynchronous and can fail. Its result is untrusted runtime data, regardless of a TypeScript annotation.

A request body, headers, and method are explicit:

js
const response = await fetch("/api/transactions", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(transaction),
});

JSON serialization omits properties whose value is undefined, cannot represent BigInt, and converts dates through their string representation. Define wire formats intentionally instead of assuming every JavaScript object round-trips.

Origins define important security boundaries ​

An origin is the combination of scheme, host, and port. Browsers apply the same-origin policy to many interactions. Cross-origin resource sharing, or CORS, lets a server opt into selected cross-origin requests through response headers.

CORS is enforced by browsers, not a client-side permission your JavaScript can grant. Adding Access-Control-Allow-Origin to the request does not solve it. The server—or a same-origin backend you control—must return the appropriate policy.

Some cross-origin requests trigger a preflight OPTIONS request. Credentials such as cookies require deliberate client and server settings, and wildcard origins cannot be combined with credentialed access.

Cookies may be automatically attached according to origin, SameSite, Secure, and credential rules. Tokens manually stored in JavaScript-accessible storage have a different threat profile. Authentication design should follow the server architecture and security review, not a generic snippet.

Model request state, not just response data ​

Loading, empty, failure, and success are different UI states:

js
let state = { status: "idle" };

async function refresh() {
  state = { status: "loading" };
  render(state);

  try {
    const transactions = await loadTransactions();
    state = transactions.length === 0
      ? { status: "empty" }
      : { status: "success", transactions };
  } catch (error) {
    state = { status: "failure", error };
  }

  render(state);
}

Repeated requests introduce ordering. If the user changes a filter quickly, an older slow response can overwrite a newer fast response. Cancellation is one solution:

js
let currentController;

async function search(query) {
  currentController?.abort();
  const controller = new AbortController();
  currentController = controller;

  try {
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });
    if (!response.ok) throw new Error(`Search failed (${response.status})`);
    renderResults(await response.json());
  } catch (error) {
    if (error.name !== "AbortError") renderError(error);
  } finally {
    if (currentController === controller) currentController = undefined;
  }
}

A monotonically increasing request ID can instead ignore stale results. Choose whether to cancel work, ignore its result, or allow concurrent results based on semantics. The renderer should also handle empty deliberately: an empty response is not a transport failure, but a blank interface does not explain that no records matched.

Browser storage is not one interchangeable bucket ​

localStorage stores string key/value pairs per origin and persists across sessions:

js
localStorage.setItem("dashboard.currency", "USD");
const currency = localStorage.getItem("dashboard.currency") ?? "USD";

It is synchronous, limited, user-clearable, and available to same-origin script. It suits small preferences, not large datasets, secrets, or a reliable database. JSON encoding adds structure but not validation:

js
function loadPreferences() {
  const text = localStorage.getItem("dashboard.preferences");
  if (text === null) return defaultPreferences;

  try {
    return parsePreferences(JSON.parse(text));
  } catch {
    return defaultPreferences;
  }
}

sessionStorage has similar string APIs but is scoped to a page session. IndexedDB is asynchronous and transactional, designed for larger structured client data. Cache Storage stores request/response pairs and is commonly used with service workers. Cookies are sent with matching HTTP requests and should not be treated as generic application storage.

The URL is often the right state container for shareable navigation state:

js
const url = new URL(window.location.href);
url.searchParams.set("currency", "EUR");
history.pushState(null, "", url);

Listen for popstate when back and forward navigation should update the UI. A state that users expect to bookmark, share, or navigate through generally belongs in the URL rather than only in memory.

Separate server state, persisted preferences, and UI state ​

These categories have different ownership:

  • Server state is a cached view of remote authority and can become stale.
  • Persisted client state survives reloads but is not inherently trustworthy.
  • Ephemeral UI state—an open menu or draft selection—often belongs only in memory.
  • URL state participates in navigation and sharing.

Libraries for querying, routing, or state management encode policies around these categories. Learn the problem before adopting a store for every value.

Offline support adds another cache and synchronization problem. A service worker can intercept requests and serve cached responses, but invalidation, upgrades, mutation conflicts, and user expectations still need explicit policy. “Works offline” is an application feature, not a checkbox produced by registering a worker.

Browser data code is strongest when each boundary is visible: HTTP status is checked, bodies are validated, stale work cannot overwrite current intent, and storage is chosen for the ownership and lifetime of the data.

Browser data sinks are security boundaries ​

Cross-site scripting is not limited to innerHTML. HTML insertion methods, srcdoc, dynamically constructed script URLs, and framework escape hatches can interpret attacker-controlled text. Prefer textContent, DOM construction, and ordinary JSX interpolation. Sanitize intentionally accepted HTML with a reviewed, explicit policy.

Validate navigation and resource URLs according to allowed schemes and origins. Content Security Policy can reduce some injection impact but does not make unsafe insertion correct.

Cookie-authenticated requests also make cross-site request forgery relevant. SameSite helps; state-changing servers may still need anti-CSRF tokens or origin checks. HttpOnly prevents JavaScript access to a cookie and Secure restricts its transport, but neither replaces authorization.

Dependencies execute with substantial authority during builds and sometimes installation. Keep direct dependencies intentional, review lockfile changes, and treat audit tools as signals rather than proofs.

Optional prompts ​

Predict: Search request A starts, request B starts later, B resolves first, and A resolves last. If both handlers render unconditionally, what does the user see?

Answer: B renders briefly, then stale result A overwrites it because completion order—not request order—controls the final write. Abort A when B begins or attach a monotonically increasing request identity and ignore any response that no longer owns the current state.

Explain: Why does adding Access-Control-Allow-Origin to a browser request not grant it cross-origin permission?

Answer: CORS is a browser-enforced policy based on response headers supplied by the server. A client cannot authorize itself. The target server must opt in, or a same-origin backend under your control must perform the cross-origin request and expose an appropriate endpoint.

Modify: A search interface should preserve filters in shared links and respect the Back button. Which state belongs in the URL, and which history operation should typing versus committed navigation use?

Answer: Shareable navigation state such as the committed query, page, and filters belongs in URLSearchParams. Use replaceState for transient edits that should not create a history entry per keystroke, and pushState for meaningful committed navigations. Listen for popstate and derive the interface from the restored URL.

Further reference ​