Frontend Engineering/
Lesson

After the overview, you know when to reach for these two. Now let's go deeper into the APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., the gotchas, and how to build something reliable on top of them. They cover the majority of real-world client-side storage needs, theme preferences, language settings, search history, draft content, form state, so getting them right matters.

The API is small by design. Once you know six methods, you know everything.

The core APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.

// Write
localStorage.setItem('key', 'value');

// Read (returns null if missing)
const value = localStorage.getItem('key');

// Delete one entry
localStorage.removeItem('key');

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

// How many items are stored
console.log(localStorage.length);

// Get a key name by index (for iterating)
const keyName = localStorage.key(0);

sessionStorage has the exact same six methods. You can swap localStorage for sessionStorage in any of the examples above.

02

Storing objects

This is the most common mistake beginners make. localStorage only accepts strings, so passing an object directly silently corrupts your data.

// This is wrong
const user = { name: 'Marie', age: 30 };
localStorage.setItem('user', user);
console.log(localStorage.getItem('user')); // '[object Object]'

// This is correct
localStorage.setItem('user', JSON.stringify(user));
const retrieved = JSON.parse(localStorage.getItem('user'));
console.log(retrieved.name); // 'Marie'
Always wrap JSON.parse in a try/catch. If someone manually edits localStorage in DevTools and corrupts the JSON, your app will throw an unhandled error.

A type-safe helper in TypeScript

Rather than scattering JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it..stringify and JSON.parse calls everywhere, wrap them once:

class TypedStorage {
  static set<T>(key: string, value: T): void {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error('Storage write failed:', e);
    }
  }

  static get<T>(key: string, defaultValue?: T): T | undefined {
    try {
      const item = localStorage.getItem(key);
      return item !== null ? (JSON.parse(item) as T) : defaultValue;
    } catch (e) {
      console.error('Storage read failed:', e);
      return defaultValue;
    }
  }

  static remove(key: string): void {
    localStorage.removeItem(key);
  }
}

// Now you get type inference everywhere
interface UserPrefs {
  theme: 'light' | 'dark';
  language: string;
}

TypedStorage.set<UserPrefs>('prefs', { theme: 'dark', language: 'en' });
const prefs = TypedStorage.get<UserPrefs>('prefs');
03

Syncing between tabs with the storage event

The storage event fires in every tab of the same originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. when localStorage changes, but not in the tab that made the change. This is genuinely useful for keeping UI in sync across multiple open windows.

window.addEventListener('storage', (event) => {
  console.log('Key changed:', event.key);
  console.log('Old value:', event.oldValue);
  console.log('New value:', event.newValue);
  console.log('Origin:', event.url);

  if (event.key === 'theme') {
    document.body.className = event.newValue || 'light';
  }
});
The storage event does NOT fire in the current tab. If your app needs to react to its own writes, dispatch a custom event alongside the localStorage write.
04

sessionStorage: the same but temporary

sessionStorage is useful whenever you want data scoped to a single user journey through your app. A multi-step checkout, a wizard, a questionnaire, any time the data is only meaningful during this visit.

// Step 1 of a multi-step form
function saveStep1(data) {
  sessionStorage.setItem('checkout_step1', JSON.stringify(data));
}

// Step 2 reads what step 1 saved
function loadStep1() {
  const raw = sessionStorage.getItem('checkout_step1');
  return raw ? JSON.parse(raw) : null;
}

// When checkout completes, clean up
function clearCheckout() {
  sessionStorage.removeItem('checkout_step1');
  sessionStorage.removeItem('checkout_step2');
}

Note that sessionStorage is per-tab, not per-browser. If the user opens your app in two tabs, each tab has its own separate sessionStorage. This is different from localStorage, which is shared across all tabs.

05

Storage limits and quota errors

Both localStorage and sessionStorage have a hard cap around 5-10MB depending on the browser. If you hit the limit, the write throws a QuotaExceededError. Always handle it.

function safeSave(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  } catch (e) {
    if (e instanceof DOMException && e.name === 'QuotaExceededError') {
      console.warn('Storage quota exceeded - consider cleaning up old data');
    }
    return false;
  }
}
06

Quick reference

MethodWhat it does
setItem(key, value)Write a string value
getItem(key)Read a value (null if missing)
removeItem(key)Delete one entry
clear()Delete all entries for this origin
lengthNumber of stored keys
key(index)Get the key name at a given index
javascript
// Complete localStorage / sessionStorage examples

// ========== 1. Preferences system ==========
const Preferences = {
  defaults: {
    theme: 'light',
    language: 'en',
    fontSize: 16,
    notifications: true
  },

  get(key) {
    const stored = localStorage.getItem('prefs');
    const prefs = stored ? JSON.parse(stored) : {};
    return prefs[key] ?? this.defaults[key];
  },

  set(key, value) {
    const stored = localStorage.getItem('prefs');
    const prefs = stored ? JSON.parse(stored) : {};
    prefs[key] = value;
    localStorage.setItem('prefs', JSON.stringify(prefs));
    window.dispatchEvent(new CustomEvent('pref-changed', { detail: { key, value } }));
  },

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

  reset() {
    localStorage.removeItem('prefs');
  }
};

Preferences.set('theme', 'dark');
console.log(Preferences.get('theme')); // 'dark'

// ========== 2. Search history ==========
const SearchHistory = {
  key: 'search_history',
  maxItems: 10,

  getAll() {
    return JSON.parse(localStorage.getItem(this.key) || '[]');
  },

  add(query) {
    let history = this.getAll().filter(item => item !== query);
    history.unshift(query);
    history = history.slice(0, this.maxItems);
    localStorage.setItem(this.key, JSON.stringify(history));
  },

  remove(query) {
    const history = this.getAll().filter(item => item !== query);
    localStorage.setItem(this.key, JSON.stringify(history));
  },

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

// ========== 3. Session-based wizard ==========
const CheckoutWizard = {
  saveStep(step, data) {
    sessionStorage.setItem(`checkout_step${step}`, JSON.stringify(data));
  },
  loadStep(step) {
    const raw = sessionStorage.getItem(`checkout_step${step}`);
    return raw ? JSON.parse(raw) : null;
  },
  complete() {
    sessionStorage.removeItem('checkout_step1');
    sessionStorage.removeItem('checkout_step2');
    sessionStorage.removeItem('checkout_step3');
  }
};