Guard browser storage

Version keys, store little, guard every call, and cache reads in memory.

From vercel-react-best-practices: client-localstorage-schema and js-cache-storage.

Ship v2 without losing prefs

Set v1 prefs, then press Deploy v2.

One unversioned keyBad
localStorage
    getItem('userConfig')
    Versioned + migrate()Good
    localStorage
      getItem(`userConfig:${VERSION}`)
      const VERSION = 'v2'
      
      function migrate() {
        try {
          const v1 = localStorage.getItem('userConfig:v1')
          if (!v1) return
          const old = JSON.parse(v1)
          saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
          localStorage.removeItem('userConfig:v1')
        } catch {}
      }

      Storage is I/O that throws

      Pick a failure mode, then flip the switches.

      Browser storage state
      UnguardedBad
      JSON.parse(localStorage.getItem(key))
      try/catch + defaultsGood
      try { … } catch { return null }
      function loadPrefs() {
        try {
          const raw = localStorage.getItem('prefs:v1')
          return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : DEFAULTS
        } catch {
          return DEFAULTS
        }
      }

      Persist only what the UI reads

      Pick fields, then run the injected script.

      Fields persisted by the good version
      Whole user objectBad
      
                
              
      setItem('user', JSON.stringify(user))
      Only what the UI readsGood
      
                
              
      setItem('prefs:v1', JSON.stringify(picked))
      localStorage.setItem('prefs:v1', JSON.stringify({
        theme: user.preferences.theme,
        notifications: user.preferences.notifications,
      }))

      Why it matters, pros and cons

      Why it matters

      Stored data outlives its code, and storage is sync I/O that can throw.

      Pros

      • Schema changes keep saved prefs.
      • Blocked storage degrades to defaults.
      • XSS finds a theme, not a token.

      Cons

      • Every version bump owes a migration.
      • An empty catch hides real bugs; report failures.
      • A read cache needs invalidation on the storage event.