How to Handle localStorage Quota Exceeded Errors

When building offline-first applications or progressive web apps, synchronous localStorage operations frequently encounter hard engine limits (~5MB per origin). Unhandled quota violations crash state persistence, block the main thread, and break user sessions. This guide details exact error interception, root cause analysis, and production-safe fallback implementations for frontend engineers and mobile web teams.

Identifying the QuotaExceededError

The localStorage API operates synchronously and lacks native buffering. When the per-origin allocation pool is exhausted, the browser throws a DOMException with name === 'QuotaExceededError'. Unlike asynchronous storage layers, localStorage does not support partial writes or deferred execution.

Detection workflow:

Storage Allocation & Serialization Overhead

Quota exhaustion rarely stems from raw text volume alone. JavaScript object serialization inflates payload size beyond the raw byte count, consuming quota faster than naive calculations predict. Unbounded arrays, missing TTL (Time-To-Live) logic, and cumulative session bloat compound the issue across user visits. Additionally, shared origin contexts—including cross-origin iframes and subdomain proxies—compete for the identical allocation pool.

Understanding synchronous blocking mechanics and origin-scoped limits is critical before implementing mitigation strategies. Review the Understanding Web Storage APIs documentation to map how payload inflation bypasses standard byte-counting assumptions.

Implement Safe Write Wrapper with Fallback

Production systems must never allow a quota violation to propagate uncaught. Wrap all setItem calls in a synchronous try/catch block, implement immediate payload compression or LRU eviction, and route overflow to asynchronous storage layers like IndexedDB or the Cache API.

The following wrapper captures the violation, attempts a single-key eviction, retries the write, and gracefully degrades to IndexedDB if the quota remains exhausted. Refer to Browser Storage Fundamentals & Quotas for cross-API migration patterns and persistence guarantees when transitioning large datasets.

/**
 * Safely writes to localStorage with automatic eviction and IndexedDB fallback.
 * @param {string} key - Storage key
 * @param {any} value - Serializable value
 * @returns {Promise<{success: boolean, fallback?: string}>}
 */
async function safeLocalStorageSet(key, value) {
 try {
 const serialized = JSON.stringify(value);
 localStorage.setItem(key, serialized);
 return { success: true };
 } catch (e) {
 if (e instanceof DOMException && e.name === 'QuotaExceededError') {
 // Basic LRU-style eviction: remove the first accessible key
 const oldestKey = localStorage.key(0);
 if (oldestKey) {
 localStorage.removeItem(oldestKey);
 }
 
 try {
 localStorage.setItem(key, JSON.stringify(value));
 return { success: true };
 } catch (retryErr) {
 console.error('Quota exceeded post-eviction. Routing to IndexedDB.');
 return await routeToIndexedDB(key, value);
 }
 }
 // Re-throw non-quota exceptions (e.g., SecurityError, TypeError)
 throw e;
 }
}

// Placeholder for IndexedDB routing implementation
async function routeToIndexedDB(key, value) {
 // Open IDB transaction, write payload, return success status
 return { success: true, fallback: 'indexeddb' };
}

Testing & Monitoring Storage Health

Relying on runtime errors alone is insufficient for offline-first architectures. Implement proactive health checks and simulate exhaustion during CI/CD pipelines.

Validation workflow:

  1. Simulate Exhaustion: Use DevTools (Application > Storage > Clear site data) combined with manual quota overrides or localStorage pre-filling scripts.
  2. Assert Fallback Paths: Verify the wrapper returns a fallback boolean and successfully triggers the IndexedDB migration route without blocking the main thread.
  3. Real-Time Telemetry: Poll navigator.storage.estimate() to track the live usage-to-quota ratio.
  4. Enforce Safety Buffers: Reject non-critical writes proactively when usage / quota > 0.90 to prevent UI thread freezes.
/**
 * Monitors storage utilization and triggers background cleanup if thresholds are breached.
 * @returns {Promise<{usageRatio: number, isCritical: boolean}>}
 */
async function validateStorageHealth() {
 const { usage, quota } = await navigator.storage.estimate();
 const usageRatio = usage / quota;
 
 // Trigger background cleanup at 80% utilization
 if (usageRatio > 0.80) {
 await triggerBackgroundCleanup();
 }
 
 return { usageRatio, isCritical: usageRatio > 0.90 };
}

async function triggerBackgroundCleanup() {
 // Implement TTL-based key pruning or cache invalidation
 console.log('Storage utilization high. Initiating background cleanup.');
}