Browser Storage

Learn how to persist data in the browser using localStorage and sessionStorage, understand how they differ from cookies, and recognize their limits and security considerations.

Sometimes a web page needs to remember something — a user's theme preference, an unsaved draft, or a shopping cart — even after the page is reloaded or the tab is closed. The browser gives JavaScript several built-in storage mechanisms for exactly this purpose, each with different lifetimes, capacities, and use cases.

localStorage

localStorage stores key/value pairs (both always strings) that persist indefinitely — even after the browser is closed and reopened — until explicitly cleared by code or by the user. Data is scoped per origin (protocol + domain + port), meaning https://example.com cannot see data stored by https://other.com.

You interact with it through a simple synchronous API: localStorage.setItem(key, value), localStorage.getItem(key), localStorage.removeItem(key), and localStorage.clear(). Because it only stores strings, non-string data (objects, arrays, numbers) must be converted with JSON.stringify() before storing and JSON.parse() after retrieving.

sessionStorage

sessionStorage has the exact same API as localStorage (setItem, getItem, removeItem, clear), but its data only lives for the duration of the page's tab session — it is cleared when that tab is closed. Reloading the page does NOT clear it, but opening the same site in a new tab starts with empty sessionStorage.

This makes sessionStorage well suited for temporary, per-tab state — like the current step of a multi-page form — that shouldn't linger after the user leaves.

Cookies overview

Cookies are an older storage mechanism, originally designed for the server rather than JavaScript. Unlike localStorage and sessionStorage, cookies are automatically sent to the server with every matching HTTP request, which makes them useful for authentication sessions but also adds overhead to every request.

Cookies can have an expiration date, can be restricted to specific paths, and can be marked HttpOnly (inaccessible to JavaScript, set only by the server) and Secure (sent only over HTTPS) for better security. document.cookie gives JavaScript access to non-HttpOnly cookies, but the API is famously awkward (a single string of 'key=value; key2=value2' pairs you must parse yourself).

Storage limits and serialization

localStorage and sessionStorage typically allow around 5–10MB per origin (the exact limit varies by browser), which is far more than cookies' roughly 4KB per cookie. Exceeding the limit causes setItem() to throw a QuotaExceededError.

Because storage only holds strings, storing structured data always requires serialization: JSON.stringify() before saving, JSON.parse() after reading. Forgetting this step is one of the most common storage bugs — beginners are often surprised to retrieve the literal string "[object Object]" instead of their data.

Security considerations

Never store sensitive data — passwords, authentication tokens meant to stay secret, credit card numbers — in localStorage or sessionStorage. Both are fully readable by any JavaScript running on the page, which means a cross-site scripting (XSS) vulnerability anywhere on the site can expose everything stored there.

For sensitive session tokens, an HttpOnly cookie set by the server is generally safer, because JavaScript (and therefore an XSS attack) cannot read it at all. Storage APIs are best used for non-sensitive convenience data like UI preferences, draft content, or cached non-secret data.

localStorage / sessionStorage API

getItem() returns null (not undefined, not an error) when the key does not exist. setItem() overwrites silently if the key already exists. sessionStorage's methods are named identically — only the persistence duration differs.

// Save (must be a string)
localStorage.setItem('theme', 'dark');

// Read
const theme = localStorage.getItem('theme'); // 'dark' or null

// Remove one key
localStorage.removeItem('theme');

// Remove everything for this origin
localStorage.clear();

// sessionStorage has the identical API, just a different lifetime
sessionStorage.setItem('formStep', '2');

Code Examples

setItem() writes a string value under a key. getItem() reads it back — note that reading a key that was never set returns null rather than throwing an error, which makes it easy to check for a value's presence with a simple if statement. Expected result: ada null

Since storage only holds strings, the preferences object is serialized with JSON.stringify() before saving. loadPreferences() reads the raw string back, falls back to sensible defaults if nothing was saved yet, and guards the JSON.parse() call with try/catch in case the stored value is somehow corrupted. Expected result: { theme: 'dark', fontSize: 18, sidebarOpen: false } localStorage.setItem('preferences', JSON.stringify(preferences));

Common mistake: Calling localStorage.setItem('preferences', preferences) directly (without JSON.stringify) stores the useless string '[object Object]' instead of the real data.

This pattern saves each step of a multi-page signup form into sessionStorage as the user progresses, so a reload doesn't lose their work. Because it is sessionStorage rather than localStorage, the partial form data automatically disappears once the tab is closed, instead of persisting forever like an abandoned draft would in localStorage. Expected result: { name: 'Ada Lovelace', email: 'ada@example.com' } sessionStorage.setItem(formStep${stepNumber}, JSON.stringify(stepData));

localStorage.setItem('username', 'ada');

const savedName = localStorage.getItem('username');
console.log(savedName); // 'ada'

const missing = localStorage.getItem('nonExistentKey');
console.log(missing); // null
const preferences = { theme: 'dark', fontSize: 18, sidebarOpen: false };

// Save: must serialize first
localStorage.setItem('preferences', JSON.stringify(preferences));

// Load: must parse, with a safe fallback
function loadPreferences() {
  const raw = localStorage.getItem('preferences');
  if (!raw) return { theme: 'light', fontSize: 16, sidebarOpen: true };
  try {
    return JSON.parse(raw);
  } catch {
    return { theme: 'light', fontSize: 16, sidebarOpen: true };
  }
}

console.log(loadPreferences());
function saveFormStep(stepNumber, stepData) {
  sessionStorage.setItem(`formStep${stepNumber}`, JSON.stringify(stepData));
}

function getAllFormData() {
  const combined = {};
  for (let i = 1; sessionStorage.getItem(`formStep${i}`); i++) {
    Object.assign(combined, JSON.parse(sessionStorage.getItem(`formStep${i}`)));
  }
  return combined;
}

saveFormStep(1, { name: 'Ada Lovelace' });
saveFormStep(2, { email: 'ada@example.com' });

console.log(getAllFormData());
// { name: 'Ada Lovelace', email: 'ada@example.com' }

Important Rules

localStorage persists indefinitely per origin; sessionStorage persists only for the current tab's lifetime.

Both store only strings — always JSON.stringify() before saving objects/arrays, and JSON.parse() after reading them.

getItem() returns null for a missing key, never undefined and never an error.

Cookies, unlike localStorage/sessionStorage, are automatically sent to the server with matching requests and have a much smaller size limit (~4KB).

Never store secrets (passwords, private tokens) in localStorage or sessionStorage — both are readable by any script on the page, including one injected via an XSS vulnerability.

Where this shows up

Dark/light theme toggles, 'remember me' preferences, unsaved draft text in a comment box, shopping cart contents before checkout, and caching non-sensitive API responses to reduce network requests are all classic uses of localStorage and sessionStorage across real websites, including EngiLipi's own theme system.

localStorage.setItem('cart', cartArray);

localStorage.setItem('cart', JSON.stringify(cartArray));

Storage APIs coerce non-string values to strings using their default toString(), which for an array or object produces useless output like '[object Object]' instead of preserving the actual data.

const token = 'secret-auth-token'; localStorage.setItem('token', token);

Use a server-set HttpOnly cookie for sensitive authentication tokens instead of localStorage.

Anything in localStorage is fully readable by JavaScript, so any XSS vulnerability on the page can steal it. HttpOnly cookies are inaccessible to JavaScript entirely, making them a safer place for sensitive session tokens.

[Multiple Choice] What is the key difference in lifetime between localStorage and sessionStorage? A. localStorage lasts until manually cleared; sessionStorage clears when the tab closes B. sessionStorage lasts forever; localStorage clears on page reload C. They have identical lifetimes but different size limits D. localStorage clears after 24 hours automatically

A. localStorage lasts until manually cleared; sessionStorage clears when the tab closes — localStorage persists across browser restarts until explicitly removed. sessionStorage is scoped to a single tab's session and is cleared automatically when that tab is closed.

[True / False] It is safe to store a user's authentication password in localStorage as long as the site uses HTTPS. A. True B. False

B. False — False. localStorage is readable by any JavaScript on the page, so an XSS vulnerability could expose it regardless of HTTPS. Sensitive credentials should never be stored there.

Write a function toggleTheme() that reads the current theme from localStorage (default 'light' if not set), switches it to the opposite value ('light' <-> 'dark'), saves the new value back to localStorage, and returns the new theme.

function toggleTheme() { const current = localStorage.getItem('theme') || 'light'; const next = current === 'light' ? 'dark' : 'light'; localStorage.setItem('theme', next); return next; }

Use getItem() with a fallback, a simple conditional to flip the value, then setItem() to persist it.