Database

Understanding LocalStorage Limitations in TypeScript

LocalStorage is simple. That's both its strength and its trap. Developers reach for it because it's easy, then discover its limitations in production.

1 May 2024

Understanding LocalStorage Limitations in TypeScript

LocalStorage is simple. That's both its strength and its trap. Developers reach for it because it's easy, then discover its limitations in production.

What LocalStorage Does

It stores key-value pairs in the browser with no expiration date. Data persists until explicitly deleted by the application or the user. It survives page reloads, browser restarts, and even system reboots.

The Limitations

5-10 MB per origin. Browsers enforce a storage quota per origin (protocol + hostname + port). Exceed it and you get a QuotaExceededError. There's no way to request more space.

Strings only. Everything stored is a string. Numbers, booleans, objects, arrays -- all get coerced to strings. You must serialize on write and parse on read.

Synchronous and blocking. Every getItem and setItem call blocks the main thread. With small data, you won't notice. With large payloads, it causes visible UI jank.

No data protection. Any JavaScript running on your page can read localStorage. That includes third-party scripts, browser extensions, and XSS payloads. Never store tokens, passwords, or sensitive data here.

No expiration mechanism. Unlike cookies, localStorage has no built-in TTL. Data sits there forever unless you manually implement expiration logic.

No cross-tab events (sort of). The storage event fires in other tabs when localStorage changes, but not in the tab that made the change. This makes cross-tab synchronization awkward.

Using LocalStorage Safely in TypeScript

Handle the quota error:

Typescript
function safeSetItem(key: string, value: string): boolean {
  try {
    localStorage.setItem(key, value);
    return true;
  } catch (e) {
    if (e instanceof DOMException && e.name === 'QuotaExceededError') {
      console.warn('LocalStorage quota exceeded');
      return false;
    }
    throw e;
  }
}

Type-safe wrapper with serialization:

Typescript
class TypedStorage<T> {
  constructor(private key: string) {}

  get(): T | null {
    const raw = localStorage.getItem(this.key);
    if (raw === null) return null;
    try {
      return JSON.parse(raw) as T;
    } catch {
      return null;
    }
  }

  set(value: T): boolean {
    try {
      localStorage.setItem(this.key, JSON.stringify(value));
      return true;
    } catch {
      return false;
    }
  }

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

const userSettings = new TypedStorage<{ theme: string; lang: string }>('settings');
userSettings.set({ theme: 'dark', lang: 'en' });
const settings = userSettings.get();

Adding manual expiration:

Typescript
interface StoredWithExpiry<T> {
  value: T;
  expiresAt: number;
}

function setWithExpiry<T>(key: string, value: T, ttlMs: number): void {
  const item: StoredWithExpiry<T> = {
    value,
    expiresAt: Date.now() + ttlMs,
  };
  localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry<T>(key: string): T | null {
  const raw = localStorage.getItem(key);
  if (!raw) return null;

  const item = JSON.parse(raw) as StoredWithExpiry<T>;
  if (Date.now() > item.expiresAt) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

When LocalStorage Is the Wrong Choice

If you need more than 10 MB, use IndexedDB. It supports structured data, async access, and much larger quotas.

If you need data to travel with HTTP requests, use cookies.

If you need server-side access to the data, use a proper database.

LocalStorage works for small, non-sensitive client-side state: user preferences, feature flags, cached UI data. Keep it small, keep it non-sensitive, and always handle the quota error.