Skip to content

Local Storage and Session Storage: Browser Data Safe

Evolution of Client-side Storage

In the early days of web development, if you wanted to store a little data in the user's browser, Cookie was almost the only choice. But Cookies have many limitations: small capacity (~4KB), automatically sent to server with every request, cumbersome API.

HTML5 brought the Web Storage API, like a small database customized for browsers—larger capacity (typically 5-10MB), simple operations, and doesn't automatically send to server. This makes client-side data storage elegant and efficient.

Web Storage comes in two flavors: localStorage provides persistent storage where data remains until explicitly deleted; sessionStorage is bound to the session, where data disappears when the tab or browser closes.

localStorage: Persistent Data Warehouse

localStorage is your permanent storage locker in the browser. As long as the user doesn't actively clear browser data, stored content will remain.

Basic Operations

javascript
// Store data
localStorage.setItem("username", "Sarah");
localStorage.setItem("theme", "dark");

// Read data
const username = localStorage.getItem("username");
console.log(username); // 'Sarah'

// Reading non-existent key returns null
const notExist = localStorage.getItem("notExist");
console.log(notExist); // null

// Delete single data
localStorage.removeItem("theme");

// Clear all data
localStorage.clear();

// Get number of stored keys
console.log(localStorage.length);

// Get key name by index
const firstKey = localStorage.key(0);
console.log(firstKey);

You can also use dot notation or bracket syntax to access directly, though this is not recommended:

javascript
// Works but not recommended
localStorage.username = "Michael";
console.log(localStorage.username);

// This approach has risks
localStorage.setItem = "oops"; // This will overwrite the setItem method!

// Always use API methods for safety
localStorage.setItem("setItem", "safe"); // Correct way

Storing Complex Data

localStorage can only store strings. If you try to store other data types, they'll be automatically converted to strings:

javascript
// Numbers get converted to strings
localStorage.setItem("count", 42);
console.log(localStorage.getItem("count")); // '42' (string)
console.log(typeof localStorage.getItem("count")); // 'string'

// Booleans too
localStorage.setItem("isActive", true);
console.log(localStorage.getItem("isActive")); // 'true' (string)

// Objects become [object Object]
localStorage.setItem("user", { name: "John" });
console.log(localStorage.getItem("user")); // '[object Object]'

To correctly store objects and arrays, you need to use JSON:

javascript
// Store object
const user = {
  id: 1,
  name: "Sarah Chen",
  email: "[email protected]",
  preferences: {
    theme: "dark",
    language: "zh-CN",
  },
};

localStorage.setItem("user", JSON.stringify(user));

// Read object
const storedUser = JSON.parse(localStorage.getItem("user"));
console.log(storedUser.name); // 'Sarah Chen'
console.log(storedUser.preferences.theme); // 'dark'

// Store array
const recentSearches = ["JavaScript", "React", "Vue", "Node.js"];
localStorage.setItem("searches", JSON.stringify(recentSearches));

// Read array
const searches = JSON.parse(localStorage.getItem("searches"));
console.log(searches[0]); // 'JavaScript'

Safe Read Wrapper

Using JSON.parse directly can throw errors if data is corrupted, so it's recommended to wrap it in a safe read function:

javascript
function getStoredItem(key, defaultValue = null) {
  try {
    const item = localStorage.getItem(key);
    if (item === null) {
      return defaultValue;
    }
    return JSON.parse(item);
  } catch (error) {
    console.warn(`Failed to read localStorage "${key}":`, error);
    return defaultValue;
  }
}

function setStoredItem(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  } catch (error) {
    console.error(`Failed to write localStorage "${key}":`, error);
    return false;
  }
}

// Usage example
setStoredItem("settings", { volume: 80, muted: false });
const settings = getStoredItem("settings", { volume: 50, muted: false });
console.log(settings.volume); // 80

Iterating All Data

javascript
// Method 1: using length and key()
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  const value = localStorage.getItem(key);
  console.log(`${key}: ${value}`);
}

// Method 2: using Object.keys()
Object.keys(localStorage).forEach((key) => {
  console.log(`${key}: ${localStorage.getItem(key)}`);
});

// Method 3: get all data as object
function getAllLocalStorage() {
  const data = {};
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    try {
      data[key] = JSON.parse(localStorage.getItem(key));
    } catch {
      data[key] = localStorage.getItem(key);
    }
  }
  return data;
}

console.log(getAllLocalStorage());

sessionStorage: Session-level Temporary Storage

sessionStorage API is identical to localStorage, differing only in data lifecycle—valid only for current session (tab).

Basic Features

javascript
// Store data
sessionStorage.setItem("currentStep", "3");
sessionStorage.setItem(
  "formData",
  JSON.stringify({
    name: "John",
    email: "[email protected]",
  })
);

// Read data
const step = sessionStorage.getItem("currentStep");
console.log(step); // '3'

// Other operations identical to localStorage
sessionStorage.removeItem("currentStep");
sessionStorage.clear();

Definition of Session

Understanding "session" in sessionStorage is important:

javascript
// Each tab has independent sessionStorage
// Same website opened in different tabs, their sessionStorage is isolated

// Tab A
sessionStorage.setItem("tabId", "A");

// Tab B (even if same website)
sessionStorage.setItem("tabId", "B");

// Each reads its own data
// Tab A reads 'A'
// Tab B reads 'B'

// Page refresh doesn't clear sessionStorage
// Closing tab clears sessionStorage
// "Reopen closed tabs" might restore sessionStorage (browser implementation varies)

Practical Application: Multi-step Form

javascript
class MultiStepForm {
  constructor(formId) {
    this.formId = formId;
    this.storageKey = `form_${formId}`;
  }

  saveProgress(step, data) {
    const formData = this.getProgress() || { steps: {} };
    formData.currentStep = step;
    formData.steps[step] = data;
    formData.lastUpdated = Date.now();

    sessionStorage.setItem(this.storageKey, JSON.stringify(formData));
  }

  getProgress() {
    const data = sessionStorage.getItem(this.storageKey);
    return data ? JSON.parse(data) : null;
  }

  getCurrentStep() {
    const progress = this.getProgress();
    return progress ? progress.currentStep : 1;
  }

  getStepData(step) {
    const progress = this.getProgress();
    return progress?.steps?.[step] || {};
  }

  clearProgress() {
    sessionStorage.removeItem(this.storageKey);
  }

  submitForm() {
    const progress = this.getProgress();
    if (!progress) return null;

    // Merge all step data
    const allData = Object.values(progress.steps).reduce(
      (acc, step) => ({ ...acc, ...step }),
      {}
    );

    // Clear session data after submit
    this.clearProgress();

    return allData;
  }
}

// Usage example
const registrationForm = new MultiStepForm("registration");

// Step 1: Basic info
registrationForm.saveProgress(1, {
  firstName: "Sarah",
  lastName: "Johnson",
});

// Step 2: Contact info
registrationForm.saveProgress(2, {
  email: "[email protected]",
  phone: "+1-234-567-8900",
});

// User refreshes page, restore progress
const currentStep = registrationForm.getCurrentStep();
console.log(`Current step: ${currentStep}`);

// Get specific step data
const step1Data = registrationForm.getStepData(1);
console.log(step1Data); // { firstName: 'Sarah', lastName: 'Johnson' }

localStorage vs sessionStorage

FeaturelocalStoragesessionStorage
LifecyclePermanent until clearedCleared on tab close
ScopeShared across all tabs (same origin)Current tab only
CapacityUsually 5-10MBUsually 5-10MB
Server sendDoesn't auto sendDoesn't auto send

Selection Guide

javascript
// Scenarios for localStorage
// ✅ User preferences
localStorage.setItem("theme", "dark");
localStorage.setItem("language", "zh-CN");

// ✅ User login status (with security policies)
localStorage.setItem("authToken", "xxx");

// ✅ Cache non-sensitive data
localStorage.setItem("cachedProducts", JSON.stringify(products));

// ✅ Remember user choices
localStorage.setItem("rememberMe", "true");

// ---

// Scenarios for sessionStorage
// ✅ Single-session temporary data
sessionStorage.setItem("currentOrder", JSON.stringify(order));

// ✅ Form drafts
sessionStorage.setItem("draftPost", JSON.stringify(postContent));

// ✅ One-time data transfer between pages
sessionStorage.setItem("redirectReason", "login_required");

// ✅ Prevent form duplicate submission
sessionStorage.setItem("formSubmitted", "true");

Storage Event Listening

When localStorage is modified in other tabs or windows, you can listen to these changes via storage event:

javascript
window.addEventListener("storage", (event) => {
  console.log("Storage changed:", {
    key: event.key, // Changed key
    oldValue: event.oldValue, // Old value
    newValue: event.newValue, // New value
    url: event.url, // Page URL triggering change
    storageArea: event.storageArea, // localStorage or sessionStorage
  });
});

// Note: storage event only triggers when modified in other tabs/windows
// Current page modifying localStorage won't trigger its own storage event

Cross-tab Communication

Using storage event, you can implement simple cross-tab communication:

javascript
class CrossTabMessenger {
  constructor(channelName) {
    this.channelName = channelName;
    this.callbacks = new Set();

    window.addEventListener("storage", (event) => {
      if (event.key === this.channelName && event.newValue) {
        const message = JSON.parse(event.newValue);
        this.callbacks.forEach((callback) => callback(message));
      }
    });
  }

  send(data) {
    const message = {
      data,
      timestamp: Date.now(),
      tabId: this.tabId,
    };

    // Set message
    localStorage.setItem(this.channelName, JSON.stringify(message));
    // Immediately delete to avoid occupying space
    localStorage.removeItem(this.channelName);
  }

  onMessage(callback) {
    this.callbacks.add(callback);
    return () => this.callbacks.delete(callback);
  }
}

// Usage example
const messenger = new CrossTabMessenger("app_messages");

// Listen for messages
messenger.onMessage((message) => {
  console.log("Message received:", message.data);
});

// Send message (received by other tabs)
messenger.send({ type: "logout", reason: "user_initiated" });

Real-time User State Sync

javascript
class UserStateSync {
  constructor() {
    this.stateKey = "user_state";
    this.listeners = [];

    // Listen for state changes in other tabs
    window.addEventListener("storage", (event) => {
      if (event.key === this.stateKey) {
        const newState = event.newValue ? JSON.parse(event.newValue) : null;
        this.notifyListeners(newState, "external");
      }
    });
  }

  setState(state) {
    localStorage.setItem(this.stateKey, JSON.stringify(state));
    this.notifyListeners(state, "internal");
  }

  getState() {
    const data = localStorage.getItem(this.stateKey);
    return data ? JSON.parse(data) : null;
  }

  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter((l) => l !== listener);
    };
  }

  notifyListeners(state, source) {
    this.listeners.forEach((listener) => listener(state, source));
  }
}

// Usage example
const userSync = new UserStateSync();

// Subscribe to state changes
userSync.subscribe((state, source) => {
  if (source === "external") {
    console.log("State updated by other tab:", state);
    // Update current page UI
    if (state?.isLoggedIn === false) {
      // User logged out in other tab
      window.location.href = "/login";
    }
  }
});

// After login, set state
userSync.setState({
  isLoggedIn: true,
  username: "Sarah",
  lastActive: Date.now(),
});

// On logout
function logout() {
  userSync.setState({ isLoggedIn: false });
  // All tabs will receive notification and redirect to login
}

Storage Limits and Error Handling

Web Storage has capacity limits and throws errors when space is insufficient:

javascript
// Detect storage capacity
function getStorageSize(storage) {
  let total = 0;
  for (let key in storage) {
    if (storage.hasOwnProperty(key)) {
      total += key.length + storage.getItem(key).length;
    }
  }
  return total;
}

console.log(`localStorage used: ${getStorageSize(localStorage)} chars`);

// Safe storage write
function safeSetItem(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
    return { success: true };
  } catch (error) {
    if (error.name === "QuotaExceededError") {
      return { success: false, error: "QUOTA_EXCEEDED" };
    }
    return { success: false, error: error.message };
  }
}

// Storage with auto-cleanup
function setItemWithCleanup(key, value, cleanupKeys = []) {
  const result = safeSetItem(key, value);

  if (!result.success && result.error === "QUOTA_EXCEEDED") {
    // Try to clean expired or unimportant data
    cleanupKeys.forEach((k) => localStorage.removeItem(k));

    // Retry
    return safeSetItem(key, value);
  }

  return result;
}

Storage with Expiration

Native Web Storage doesn't support expiration, but you can implement it yourself:

javascript
class ExpiringStorage {
  static set(key, value, ttlMs) {
    const item = {
      value,
      expiry: ttlMs ? Date.now() + ttlMs : null,
    };
    localStorage.setItem(key, JSON.stringify(item));
  }

  static get(key) {
    const itemStr = localStorage.getItem(key);
    if (!itemStr) return null;

    try {
      const item = JSON.parse(itemStr);

      // Check if expired
      if (item.expiry && Date.now() > item.expiry) {
        localStorage.removeItem(key);
        return null;
      }

      return item.value;
    } catch {
      return null;
    }
  }

  static remove(key) {
    localStorage.removeItem(key);
  }

  // Clean all expired items
  static cleanup() {
    const keysToRemove = [];

    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      try {
        const item = JSON.parse(localStorage.getItem(key));
        if (item.expiry && Date.now() > item.expiry) {
          keysToRemove.push(key);
        }
      } catch {
        // Not our data format, skip
      }
    }

    keysToRemove.forEach((key) => localStorage.removeItem(key));
    return keysToRemove.length;
  }
}

// Usage example
// Cache data for 1 hour
ExpiringStorage.set("apiResponse", { data: [1, 2, 3] }, 60 * 60 * 1000);

// Get data (returns null if expired)
const cached = ExpiringStorage.get("apiResponse");

// Periodically clean expired data
setInterval(() => {
  const cleaned = ExpiringStorage.cleanup();
  if (cleaned > 0) {
    console.log(`Cleaned ${cleaned} expired items`);
  }
}, 60 * 1000); // Check every minute

Practical Application Scenarios

User Preferences

javascript
class UserPreferences {
  constructor() {
    this.storageKey = "userPreferences";
    this.defaults = {
      theme: "light",
      fontSize: "medium",
      language: "en",
      notifications: true,
      sidebar: "expanded",
    };
  }

  get(key) {
    const prefs = this.getAll();
    return prefs[key];
  }

  set(key, value) {
    const prefs = this.getAll();
    prefs[key] = value;
    localStorage.setItem(this.storageKey, JSON.stringify(prefs));

    // Trigger change event
    window.dispatchEvent(
      new CustomEvent("preferencesChanged", {
        detail: { key, value },
      })
    );
  }

  getAll() {
    try {
      const stored = localStorage.getItem(this.storageKey);
      return stored
        ? { ...this.defaults, ...JSON.parse(stored) }
        : this.defaults;
    } catch {
      return this.defaults;
    }
  }

  reset() {
    localStorage.removeItem(this.storageKey);
    window.dispatchEvent(
      new CustomEvent("preferencesChanged", {
        detail: { reset: true },
      })
    );
  }
}

// Usage example
const prefs = new UserPreferences();

// Get setting
console.log(prefs.get("theme")); // 'light'

// Update setting
prefs.set("theme", "dark");
prefs.set("fontSize", "large");

// Listen for setting changes
window.addEventListener("preferencesChanged", (event) => {
  const { key, value, reset } = event.detail;

  if (reset) {
    console.log("Settings reset");
    // Reapply default settings
  } else {
    console.log(`Setting updated: ${key} = ${value}`);
    // Apply new setting
    if (key === "theme") {
      document.body.className = value;
    }
  }
});

Shopping Cart

javascript
class ShoppingCart {
  constructor() {
    this.storageKey = "shopping_cart";
  }

  getItems() {
    const data = localStorage.getItem(this.storageKey);
    return data ? JSON.parse(data) : [];
  }

  addItem(product, quantity = 1) {
    const items = this.getItems();
    const existingIndex = items.findIndex((item) => item.id === product.id);

    if (existingIndex >= 0) {
      items[existingIndex].quantity += quantity;
    } else {
      items.push({
        id: product.id,
        name: product.name,
        price: product.price,
        image: product.image,
        quantity,
      });
    }

    this.save(items);
    return items;
  }

  updateQuantity(productId, quantity) {
    const items = this.getItems();
    const index = items.findIndex((item) => item.id === productId);

    if (index >= 0) {
      if (quantity <= 0) {
        items.splice(index, 1);
      } else {
        items[index].quantity = quantity;
      }
      this.save(items);
    }

    return items;
  }

  removeItem(productId) {
    const items = this.getItems().filter((item) => item.id !== productId);
    this.save(items);
    return items;
  }

  clear() {
    localStorage.removeItem(this.storageKey);
  }

  save(items) {
    localStorage.setItem(this.storageKey, JSON.stringify(items));
  }

  getTotal() {
    const items = this.getItems();
    return items.reduce((total, item) => {
      return total + item.price * item.quantity;
    }, 0);
  }

  getItemCount() {
    const items = this.getItems();
    return items.reduce((count, item) => count + item.quantity, 0);
  }
}

// Usage example
const cart = new ShoppingCart();

// Add items
cart.addItem({ id: 1, name: "Wireless Mouse", price: 29.99 }, 2);
cart.addItem({ id: 2, name: "USB Keyboard", price: 49.99 }, 1);

// Get cart info
console.log("Item count:", cart.getItemCount()); // 3
console.log("Total:", cart.getTotal().toFixed(2)); // 109.97

// Update quantity
cart.updateQuantity(1, 3);

// Remove item
cart.removeItem(2);

Recently Viewed

javascript
class RecentlyViewed {
  constructor(options = {}) {
    this.storageKey = options.key || "recently_viewed";
    this.maxItems = options.maxItems || 10;
  }

  add(item) {
    const items = this.getAll();

    // Remove existing same item
    const filtered = items.filter((i) => i.id !== item.id);

    // Add to beginning
    filtered.unshift({
      ...item,
      viewedAt: Date.now(),
    });

    // Maintain max item limit
    const limited = filtered.slice(0, this.maxItems);

    localStorage.setItem(this.storageKey, JSON.stringify(limited));
    return limited;
  }

  getAll() {
    try {
      const data = localStorage.getItem(this.storageKey);
      return data ? JSON.parse(data) : [];
    } catch {
      return [];
    }
  }

  clear() {
    localStorage.removeItem(this.storageKey);
  }

  remove(id) {
    const items = this.getAll().filter((item) => item.id !== id);
    localStorage.setItem(this.storageKey, JSON.stringify(items));
    return items;
  }
}

// Usage example
const recentlyViewed = new RecentlyViewed({ maxItems: 5 });

// User views product
recentlyViewed.add({ id: 101, name: "Product A", price: 99.99 });
recentlyViewed.add({ id: 102, name: "Product B", price: 149.99 });
recentlyViewed.add({ id: 103, name: "Product C", price: 79.99 });

// Show recently viewed
const recent = recentlyViewed.getAll();
console.log("Recently viewed:", recent);

Security Considerations

Although Web Storage is convenient, there are security risks to be aware of:

javascript
// ⚠️ Don't store sensitive information
// Following are wrong examples:
localStorage.setItem("password", "mySecret123"); // ❌ NEVER do this
localStorage.setItem("creditCard", "4111111111111111"); // ❌ NEVER do this

// ⚠️ XSS attacks can access localStorage
// If website has XSS vulnerability, attacker can execute:
// console.log(localStorage.getItem('authToken'));

// ✅ Security recommendations:
// 1. Validate stored data
function getValidatedItem(key, validator) {
  try {
    const value = JSON.parse(localStorage.getItem(key));
    if (validator(value)) {
      return value;
    }
    localStorage.removeItem(key); // Invalid data, clear it
    return null;
  } catch {
    return null;
  }
}

// 2. For sensitive operations use httpOnly Cookie, not localStorage
// 3. Implement Content Security Policy (CSP) to prevent XSS
// 4. Strictly validate and sanitize user input

Summary

Web Storage API provides a simple and powerful solution for client-side data storage. localStorage is suitable for long-term storage needs like user preferences and cached data; sessionStorage is suitable for session-level storage scenarios like temporary data and form drafts.

Key takeaways:

  • Can only store strings, complex data needs JSON serialization
  • localStorage shared across tabs, sessionStorage isolated per tab
  • Use storage event for cross-tab communication
  • Watch storage capacity limits, handle errors properly
  • Don't store sensitive information, beware of XSS protection

Reasonable use of Web Storage can significantly improve user experience, making your web application smarter and more personalized.