TL;DR: localStorage is a key-value store built into every web browser. It lets JavaScript save small amounts of text data (up to ~5MB) that persists even after the browser is closed. It is perfect for non-sensitive things like dark mode settings, saved form drafts, and shopping cart contents. It is completely wrong — and actively dangerous — for passwords, auth tokens, or anything private, because any JavaScript on the page can read it.

Why AI Coders Need This

If you have ever asked Claude, ChatGPT, or Cursor to build a web app that "remembers" anything between visits, it used localStorage. Every single time. It is AI's go-to move for client-side persistence because it requires zero infrastructure — no database, no backend, no configuration. Three methods and your data outlives the browser session.

That simplicity is also why you need to understand it. AI will correctly use localStorage to remember your dark mode preference. It will also — without hesitation, without warning — suggest storing authentication tokens there. One is fine. One is a security vulnerability.

You cannot catch that mistake if you do not know what localStorage is and what it is not. That is the whole point of this article.

You will see localStorage come up constantly across web development topics: JSON (because localStorage only stores strings and you need JSON.stringify/parse), cookies (the safer alternative for auth), and databases (the right tool when localStorage is not enough). We will link all of those at the end.

The Sticky Notes Analogy

localStorage is like sticky notes on your monitor. You jot down a quick reminder — "dark mode on," "cart has 3 items," "last-viewed tab was Settings" — and it is there every time you glance over. Fast, convenient, always visible.

But you would never write your bank password on a sticky note and leave it on your desk. Anyone who walks by can read it. You also would not try to write a novel on sticky notes — limited space. And if someone sits at a different computer, they cannot see the sticky notes on yours — each browser has its own storage.

That is localStorage in a sentence: quick reminders per browser, with a strict size limit, visible to anyone on the page.

Real Scenario

You are building a simple productivity app. No backend yet — just HTML and JavaScript. You want three things to persist across page refreshes:

  • A dark mode toggle — user preference that should stick
  • User preferences — their chosen display name and notification settings
  • A shopping cart — items they added before checking out

All three of these are perfect localStorage candidates: non-sensitive, user-specific to this browser, and small enough to fit comfortably in 5MB.

Prompt I Would Type

Build a simple product page with three features that persist after 
page refresh:

1. A dark/light mode toggle button — remember which mode the user 
   picked
2. A user preferences panel where they can set a display name and 
   toggle email notifications on/off — remember their choices
3. A shopping cart — when they click "Add to Cart," the item stays 
   even after closing and reopening the browser

Use localStorage for all three. Comment every localStorage call 
and explain what would break if we forgot to use JSON.stringify 
or JSON.parse.

That last instruction matters. Ask AI to explain the tricky parts — especially JSON.stringify and JSON.parse — because this is where most localStorage bugs come from.

What AI Generated

Here is the core localStorage code AI would write for all three use cases. Read the comments — this is exactly what you need to understand:

// ============================================================
// 1. DARK MODE TOGGLE
// Stores a simple string: "dark" or "light"
// ============================================================

function saveDarkMode(isDark) {
  // setItem(key, value) — both must be strings
  // 'theme' is the key; we choose this name, it can be anything
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
}

function loadDarkMode() {
  // getItem(key) — returns the stored string, or null if not set
  const theme = localStorage.getItem('theme');
  return theme === 'dark'; // Convert string back to boolean
}

// Apply theme on page load
if (loadDarkMode()) {
  document.body.classList.add('dark-mode');
}


// ============================================================
// 2. USER PREFERENCES
// Stores an object — requires JSON.stringify / JSON.parse
// ============================================================

function savePreferences(prefs) {
  // localStorage can ONLY store strings.
  // JSON.stringify converts { name: "Chuck", notifications: true }
  // into the string '{"name":"Chuck","notifications":true}'
  // Without this, localStorage stores "[object Object]" — useless.
  localStorage.setItem('userPrefs', JSON.stringify(prefs));
}

function loadPreferences() {
  const stored = localStorage.getItem('userPrefs');

  // getItem returns null if the key has never been set
  if (!stored) {
    return { name: '', notifications: true }; // Default values
  }

  // JSON.parse converts the string back into a real JS object.
  // ⚠️ Always wrap in try/catch — corrupted JSON will crash your app.
  try {
    return JSON.parse(stored);
  } catch (err) {
    console.error('Could not parse saved preferences:', err);
    localStorage.removeItem('userPrefs'); // Clear the bad data
    return { name: '', notifications: true };
  }
}

// Usage
savePreferences({ name: 'Chuck', notifications: false });
const prefs = loadPreferences(); // { name: 'Chuck', notifications: false }


// ============================================================
// 3. SHOPPING CART
// Stores an array of cart items — also needs JSON.stringify/parse
// ============================================================

function getCart() {
  try {
    return JSON.parse(localStorage.getItem('cart')) || [];
  } catch (err) {
    return []; // If cart data is corrupted, start fresh
  }
}

function addToCart(product) {
  const cart = getCart();
  const existingIndex = cart.findIndex(item => item.id === product.id);

  if (existingIndex > -1) {
    // Product already in cart — increment quantity
    cart[existingIndex].quantity += 1;
  } else {
    // New product — add with quantity 1
    cart.push({ ...product, quantity: 1 });
  }

  // Wrap setItem in try/catch to handle the QuotaExceededError
  // (thrown when ~5MB localStorage limit is reached)
  try {
    localStorage.setItem('cart', JSON.stringify(cart));
  } catch (err) {
    console.error('Could not save cart — storage may be full:', err);
  }
}

function removeFromCart(productId) {
  const cart = getCart().filter(item => item.id !== productId);
  localStorage.setItem('cart', JSON.stringify(cart));
}

function clearCart() {
  // removeItem deletes a single key from localStorage
  localStorage.removeItem('cart');

  // FYI: localStorage.clear() removes ALL keys for this origin —
  // it would wipe cart AND preferences AND theme. Be careful.
}

Three different storage patterns. All use the same three methods. The only thing that changes is whether the value is a simple string (dark mode) or a complex object that needs JSON conversion (preferences, cart).

Understanding Each Part

setItem — saving data

localStorage.setItem('key', 'value') stores a string under a name you choose. The key is how you will retrieve it later. If a value already exists for that key, it gets overwritten silently. Both key and value must be strings — anything else gets coerced, usually into something you did not want.

getItem — reading data

localStorage.getItem('key') returns the stored string, or null if that key has never been set. Always check for null before doing anything with the result — especially before calling JSON.parse(), which will throw on a null value.

removeItem — deleting one key

localStorage.removeItem('key') deletes a single entry. Data is gone immediately. Use this when users log out, reset their preferences, or empty their cart.

clear — nuclear option

localStorage.clear() wipes all localStorage data for your origin in one call. Useful during testing; dangerous in production if you have multiple features sharing storage. Prefer removeItem for targeted cleanup.

JSON.stringify and JSON.parse — the required dance

localStorage is a string-only store. The moment you try to save an object or array without converting it first, you get "[object Object]" stored instead of your actual data. JSON.stringify() converts JavaScript values to strings; JSON.parse() converts them back. Our full guide on What Is JSON? covers this conversion in depth.

The 5MB limit

Each origin (domain + protocol + port combination) gets roughly 5MB. That is about 5 million characters of text — plenty for preferences and cart data, but easy to blow past if AI stores images as base64 strings or caches large API responses there. When you exceed it, setItem throws QuotaExceededError.

Same-origin policy

Data stored at https://myapp.com is invisible to https://other-site.com. Even http://myapp.com (no S) cannot see data stored by https://myapp.com — different protocol, different storage bucket. This also means localStorage on localhost:3000 during development is separate from your production domain, so users do not get their dev-session data in production.

It blocks while running

localStorage reads and writes are synchronous — they pause everything else while they execute. For normal use (small strings, occasional saves), this is imperceptible. For large datasets or tight loops, it can visibly freeze the page. If AI generates code reading localStorage inside a loop that runs hundreds of times, that is a performance problem worth flagging.

localStorage vs sessionStorage vs Cookies

AI defaults to localStorage, but two other browser storage mechanisms might be a better fit. Here is how they compare side by side:

Feature localStorage sessionStorage Cookies
Capacity ~5MB ~5MB ~4KB per cookie
Lifespan Until deleted by code or user Tab close = gone Set by expiry date
Sent to server? ✗ No ✗ No ✓ Yes (every request)
JavaScript can read? ✓ Always ✓ Always Only if not httpOnly
XSS-safe? ✗ No ✗ No ✓ Yes (if httpOnly)
Works cross-tab? ✓ Yes ✗ Tab-isolated ✓ Yes
Best for User prefs, cart, UI state Multi-step forms, temp state Auth tokens, server-accessible data

The decision tree: if data needs to reach your server (authentication, tracking), use cookies. If data should vanish when the tab closes (checkout flow progress, wizard steps), use sessionStorage. For everything else non-sensitive that should persist, use localStorage.

What AI Gets Wrong About localStorage

AI generates correct localStorage code most of the time. These three mistakes are the exceptions — and they range from "your app will behave weirdly" to "your users' accounts get compromised."

Mistake 1: Storing Sensitive Data — Passwords, Tokens, API Keys

This is the serious one. AI will regularly store JWT tokens or session tokens in localStorage right after login:

// ❌ What AI often generates — a real security risk
async function login(email, password) {
  const res = await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ email, password }),
    headers: { 'Content-Type': 'application/json' }
  });
  const data = await res.json();

  // ❌ This stores your auth token where any JS on the page can read it
  localStorage.setItem('authToken', data.token);
}

// ✅ The safe approach: use httpOnly cookies
// Your server sets the cookie in the response header — JavaScript never touches it:
// Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict
// The browser stores and sends it automatically on every request.
// Even if an attacker injects JS into your page, they cannot read httpOnly cookies.

Why it matters: Cross-Site Scripting (XSS) attacks inject malicious JavaScript into your page — through unsanitized user input, compromised third-party scripts, or browser extensions. That injected script has full access to localStorage. It grabs the auth token and sends it to the attacker's server. Now they are logged in as your user. httpOnly cookies are immune to this because JavaScript — including the attacker's code — literally cannot read them.

The fix prompt:

This code stores the auth token in localStorage, which is vulnerable 
to XSS attacks. Refactor to use httpOnly cookies instead. The server 
should set the cookie via Set-Cookie response header with HttpOnly, 
Secure, and SameSite=Strict flags. JavaScript should never directly 
handle the auth token.

Mistake 2: No Error Handling When Storage Is Full

AI almost never wraps setItem in a try/catch. When localStorage hits its ~5MB limit, setItem throws a QuotaExceededError and the data is silently not saved — no warning to the user, no indication anything went wrong:

// ❌ Data silently fails to save when storage is full
localStorage.setItem('cachedData', JSON.stringify(bigDataset));

// ✅ Catch the error and handle it gracefully
try {
  localStorage.setItem('cachedData', JSON.stringify(bigDataset));
} catch (err) {
  if (err.name === 'QuotaExceededError') {
    // Option 1: Clear old cached data to make room
    localStorage.removeItem('oldCache');
    // Option 2: Warn the user
    console.warn('Storage full — some data could not be saved.');
    // Option 3: Fall back to in-memory storage for this session
  }
}

This is especially important if AI generates code that caches API responses, stores images, or accumulates data over time. The 5MB limit sounds generous until you are storing product catalogs or user-generated content.

Mistake 3: Forgetting JSON.parse (or Not Catching Its Errors)

AI sometimes retrieves stored data and uses it directly without parsing — treating a JSON string as if it were already a JavaScript object. Other times it parses but skips the try/catch, which means a single corrupted localStorage entry crashes the entire app:

// ❌ Reading raw string and treating it like an object
const prefs = localStorage.getItem('userPrefs');
console.log(prefs.name); // undefined — prefs is a string, not an object

// ❌ Parsing without safety net — crashes if data is corrupted
const prefs = JSON.parse(localStorage.getItem('userPrefs'));

// ✅ The correct pattern: null check + try/catch
let prefs = { name: '', notifications: true }; // safe default
const stored = localStorage.getItem('userPrefs');
if (stored) {
  try {
    prefs = JSON.parse(stored);
  } catch (err) {
    // Data was corrupted — maybe user manually edited it in DevTools,
    // a browser extension modified it, or a partial write corrupted it.
    console.error('Bad localStorage data, resetting to defaults:', err);
    localStorage.removeItem('userPrefs'); // Clear the corrupted entry
  }
}

localStorage data can get corrupted in surprising ways. Users edit it directly in DevTools. Browser extensions overwrite it. Power loss during a write can leave partial JSON. The try/catch is not paranoia — it is basic resilience.

The localStorage Golden Rule

Never store anything you would not want a stranger to read. localStorage is a sticky note on a shared monitor. Treat it that way: great for display preferences and cart contents, completely wrong for passwords, tokens, and personal data.

How to Debug localStorage With AI

Inspect it in DevTools

Open Chrome DevTools (Cmd+Option+I on Mac, F12 on Windows), go to the Application tab, and expand Local Storage in the left sidebar. Click your origin. Every key-value pair stored by your page appears here. You can edit values directly, delete individual keys, or right-click to clear all.

This is your first debugging stop. If your app is not loading saved data, check here: is the key actually there? Is the value valid JSON or does it say [object Object]?

Quick console checks

// See everything stored for this site
Object.entries(localStorage).forEach(([key, val]) => {
  console.log(key, '→', val);
});

// Validate a specific value is proper JSON
try {
  JSON.parse(localStorage.getItem('cart'));
  console.log('Cart: valid JSON ✓');
} catch (e) {
  console.log('Cart: corrupted JSON ✗', e.message);
}

// Rough storage usage estimate
let bytes = 0;
for (const key in localStorage) {
  if (localStorage.hasOwnProperty(key)) {
    bytes += key.length + localStorage[key].length;
  }
}
console.log(`Using ~${(bytes / 1024).toFixed(1)} KB of ~5120 KB`);

Debug prompt that actually works

Debug Prompt

My app uses localStorage but data keeps disappearing or loading 
wrong. Here is my save/load code:

[paste your code]

Check it for:
1. Missing JSON.stringify when saving objects/arrays
2. Missing JSON.parse when reading (or missing try/catch around it)
3. Missing null check when getItem returns nothing
4. No error handling for QuotaExceededError on setItem
5. Whether this code would break in a server-side rendering context
   like Next.js (window.localStorage does not exist on the server)

What to Learn Next

localStorage is one piece of a larger picture. These four articles fill in the concepts that connect directly to what you just learned:

Frequently Asked Questions

What is localStorage in simple terms?

localStorage is a small storage area built into every web browser. It lets your JavaScript save key-value pairs of text that persist even after the browser closes and reopens. Think of it as a sticky-note system for your website: data you put there stays until your code deletes it or the user clears their browser data. It stores about 5MB of data per website and never touches the server — it is entirely client-side.

Is localStorage safe for storing passwords or auth tokens?

No — and this is the most important thing to know about localStorage. It is readable by any JavaScript running on your page, including malicious scripts injected through XSS attacks. Never store passwords, authentication tokens, API keys, or sensitive personal data there. Use httpOnly cookies for auth tokens — JavaScript literally cannot read them, making them immune to XSS theft. If AI generates code that puts tokens in localStorage, ask it to refactor to httpOnly cookies immediately.

What is the difference between localStorage and sessionStorage?

localStorage persists until your code explicitly deletes it — it survives closing the browser, restarting the computer, and system updates. sessionStorage is wiped the moment the browser tab is closed. They share identical APIs (setItem, getItem, removeItem, clear) and the same ~5MB limit. The rule of thumb: use sessionStorage for data that should not outlive the current session — like a multi-step checkout flow, a wizard in progress, or a temporary filter state. Use localStorage for persistent preferences and non-sensitive data you want to survive between visits.

Can I store objects and arrays in localStorage?

Not directly. localStorage only stores strings. To save a JavaScript object or array, convert it to a JSON string first using JSON.stringify(), then convert it back when you read it with JSON.parse(). Skipping this means localStorage stores "[object Object]" instead of your actual data. Always wrap JSON.parse() in a try/catch — if the stored string is corrupted (manually edited, partial write, browser extension interference), JSON.parse throws a SyntaxError that crashes your app without one. See our What Is JSON? guide for the full picture.

What happens when localStorage is full?

localStorage.setItem() throws a QuotaExceededError when the ~5MB limit is reached. The data is not saved, and if your code does not catch this error, the failure is completely silent — your app behaves as if it saved successfully but the data is gone. Always wrap setItem in a try/catch so you can handle this gracefully: clear old cached data, warn the user, or fall back to in-memory storage. You can monitor usage with a loop over localStorage keys to estimate how close you are to the limit. To free up space during testing, go to Chrome DevTools → Application → Local Storage → right-click your origin → Clear.