Appearance
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 = { 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.
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.