Browser storage looks simple until it becomes part of an authentication flow, a multi-tab experience, or an application that must survive schema changes.
- The API is rarely the difficult part. The decision is who needs the data, how long it should live, and which security boundary it crosses.
- Browsers do not expose a standard API named
cookieStorage. Traditional cookies go throughdocument.cookie; the newer asynchronous Cookie Store API goes throughcookieStore. They manage the same underlying cookies, and neither behaves like Web Storage.
Overview
| Property | localStorage | sessionStorage | Cookies |
|---|---|---|---|
| Typical lifetime | Until explicitly cleared or evicted | Current tab's page session | Session-based or until their expiry date |
| Sent to server | No | No | Yes, on matching requests |
| JavaScript access | Yes, synchronously | Yes, synchronously | Yes, unless marked HttpOnly |
| Best suited for | Persistent, non-sensitive preferences | Temporary state that should survive reloads | Server-managed sessions and small server-readable values |
- Web Storage belongs to the client. Cookies participate in the HTTP request lifecycle.
- None should be treated as a trusted database.
localStorage
localStorage persists data for an origin across page reloads and browser restarts.
ts
const preferences = {
theme: "dark",
density: "compact",
}
localStorage.setItem("preferences:v1", JSON.stringify(preferences))
const stored = localStorage.getItem("preferences:v1")
const parsed = stored ? JSON.parse(stored) : nullts
type Preferences = {
theme: "light" | "dark"
}
function readPreferences(): Preferences | null {
try {
const value = JSON.parse(localStorage.getItem("preferences:v1") ?? "null")
if (value?.theme === "light" || value?.theme === "dark") {
return value
}
} catch {
// Corrupt or manually edited storage should not break the application.
}
return null
}- Useful for small, non-sensitive preferences: a theme, a dismissed notice, an unfinished local draft.
- Values are strings, access is synchronous, and every read or write blocks the main thread. Capacity and eviction vary by browser. It is a small persistence mechanism, not a database.
- Version keys and validate on read: deployed applications change while old browser data remains.
- Failure: session tokens, passwords, or long-lived bearer tokens in
localStorage. Any script on the origin can read it, including XSS. Stealing the value is enough to reuse it elsewhere.
sessionStorage
sessionStorage has a similar string-based API, but its lifetime is tied to a page session.
ts
sessionStorage.setItem(
"checkout:draft",
JSON.stringify({ step: 2, deliveryMethod: "pickup" })
)- It survives reloads in the same tab and is normally cleared when that tab or window closes. Separate tabs get separate storage.
- A good fit for temporary UI state: a multi-step form, a return URL, or filters that should survive an accidental refresh without becoming a permanent preference.
- Failure: treating
sessionStorageas a security boundary. Scripts on the page can still read it, and users can duplicate or restore tabs in ways that make lifecycle assumptions less obvious. It fits when its product lifetime matches the data, not because it is safer thanlocalStorage.
Cookies and the Cookie Store API
Cookies are different because the browser can attach them to HTTP requests.
http
Set-Cookie: session=opaque-value; Path=/; HttpOnly; Secure; SameSite=Laxts
const preference = await cookieStore.get("theme")
await cookieStore.set({
name: "theme",
value: "dark",
path: "/",
sameSite: "lax",
})- Appropriate when the server needs the value, especially for server-managed sessions. Security-sensitive cookies should usually be created by the server.
HttpOnlyprevents JavaScript from reading the cookie,Securerestricts it to HTTPS, andSameSitehelps limit CSRF. These reduce risk; they do not replace output escaping, CSP, CSRF analysis, or session expiration and rotation.- Traditional
document.cookieis synchronous and awkward to parse. The Cookie Store API is asynchronous where supported. Client-side code still cannot read anHttpOnlycookie throughcookieStore— that restriction is the point. - Failure: stuffing preferences into cookies because
cookieStorelooks likelocalStorage. Cookies are small, domain and path rules are subtle, and cookies included with requests add network overhead every time.
Defaults
Store the least data for the shortest useful lifetime.
- Use in-memory state when the data only needs to live while the current page is open.
- Use sessionStorage when temporary state should survive reloads in one tab.
- Use localStorage for small, non-sensitive preferences that should persist across visits.
- Use cookies when the server must receive the value, particularly for an opaque session identifier in a secure, HTTP-only cookie.
- Use IndexedDB instead when the application needs significant data, structured records, transactions, or offline behavior.
- Browser storage is controlled by the user, can be cleared or modified, and should never be treated as an authoritative source of truth. Validate on read, handle storage failures, and keep server-side authorization independent of anything the client can edit.
Choosing storage is an architecture decision, not an API preference. Start with the data's lifetime and trust level; the correct browser primitive usually follows.
Recap Q&A
Read the next note
Core JavaScript Concepts