How to Fix QuotaExceededError in localStorage (Prevent App Crashes)
I got a call at 2 AM. Our production app was down. Users couldn’t save their work. The error logs showed QuotaExceededError: DOM Exception 22. I had no idea localStorage could crash my app.
The Problem
I was treating localStorage like free real estate. Store user preferences? localStorage. Cache API responses? localStorage. Remember form state? localStorage. It all worked fine in development.
But in production, some users had accumulated months of data. When localStorage hit the browser’s limit (5-10 MiB depending on the browser), everything broke. The setItem() call threw an exception that propagated up and crashed the entire application.
Here’s the dangerous code I had:
// DANGEROUS: This crashes when localStorage is fullfunction saveUserPreferences(prefs) { localStorage.setItem('userPrefs', JSON.stringify(prefs)); return true;}
function cacheSearchResults(query, results) { localStorage.setItem(`search_${query}`, JSON.stringify(results));}No try-catch. No error handling. Just raw, optimistic setItem() calls. When the quota was exceeded, the exception bubbled up and broke the user’s workflow.
Why This Happens
Browser storage isn’t unlimited. According to MDN Web Docs, browsers enforce strict quotas:
- 5 MiB for localStorage per origin
- 5 MiB for sessionStorage per origin
- 10 MiB total across both storage types
When you exceed these limits, browsers throw QuotaExceededError (DOM Exception 22). This isn’t a warning or a graceful degradation—it’s an exception that will crash your code if you don’t catch it.
Common causes I’ve seen:
- Base64 encoded images - Someone thought storing images in localStorage for “caching” was a good idea
- Unbounded data accumulation - Never clearing old entries, just adding more
- Massive JSON objects - Storing entire API responses without trimming
- Multiple tabs multiplying the problem - Each tab’s data adds up
The Fix
The solution is straightforward: wrap every localStorage.setItem() call in a try-catch block. Here’s how I fixed my code:
function saveUserPreferences(prefs) { try { localStorage.setItem('userPrefs', JSON.stringify(prefs)); return true; } catch (error) { if (error.name === 'QuotaExceededError' || error.code === 22) { console.warn('localStorage quota exceeded. Clearing old data...'); // Clear and retry clearOldData(); try { localStorage.setItem('userPrefs', JSON.stringify(prefs)); return true; } catch (retryError) { console.error('Still cannot save after clearing:', retryError); return false; } } console.error('Unexpected localStorage error:', error); return false; }}But I didn’t want to write this try-catch logic every time. So I created a helper function:
/** * Safely store data in localStorage with error handling * @param {string} key - Storage key * @param {any} value - Value to store (will be JSON stringified) * @returns {boolean} - True if saved successfully, false otherwise */function safeSetItem(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { if (error.name === 'QuotaExceededError' || error.code === 22) { console.warn(`Storage quota exceeded for key: ${key}`); return false; } console.error('localStorage error:', error); return false; }}
/** * Safely retrieve data from localStorage * @param {string} key - Storage key * @param {any} defaultValue - Value to return if key doesn't exist * @returns {any} - Parsed value or default */function safeGetItem(key, defaultValue = null) { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (error) { console.error(`Error reading localStorage key ${key}:`, error); return defaultValue; }}
/** * Check if localStorage is available and working * @returns {boolean} */function isStorageAvailable() { try { const test = '__storage_test__'; localStorage.setItem(test, test); localStorage.removeItem(test); return true; } catch (e) { return false; }}Now my code looks like this:
// Much cleaner and saferif (safeSetItem('userPrefs', preferences)) { console.log('Preferences saved');} else { // Handle the failure gracefully showNotification('Storage full. Preferences not saved.'); // Maybe save to server instead savePreferencesToServer(preferences);}Checking Storage Before Writing
I also wanted to know how much space I had left. Here’s a helper that estimates available storage:
/** * Estimate localStorage usage * @returns {object} - Usage statistics */function getStorageStats() { let totalSize = 0;
for (let key in localStorage) { if (localStorage.hasOwnProperty(key)) { const value = localStorage.getItem(key); // Each character is 2 bytes in UTF-16 totalSize += (key.length + value.length) * 2; } }
// Convert to KB const sizeKB = totalSize / 1024;
// Most browsers allow ~5 MB (5120 KB) const quotaKB = 5120; const percentUsed = (sizeKB / quotaKB) * 100;
return { usedKB: Math.round(sizeKB), quotaKB: quotaKB, percentUsed: Math.round(percentUsed), availableKB: Math.round(quotaKB - sizeKB) };}
// Usageconst stats = getStorageStats();if (stats.percentUsed > 80) { console.warn(`Storage is ${stats.percentUsed}% full. Consider clearing old data.`);}Handling the Inevitable Cleanup
When storage is full, you need strategies to clear space. Here’s an LRU (Least Recently Used) approach:
/** * Store data with timestamp for LRU eviction */function setWithTimestamp(key, value) { const data = { value: value, timestamp: Date.now() }; return safeSetItem(key, data);}
/** * Clear oldest entries to make space * @param {number} bytesToFree - Approximate bytes to free */function clearOldestEntries(bytesToFree = 102400) { // Default: 100KB const entries = [];
// Collect all entries with timestamps for (let key in localStorage) { if (localStorage.hasOwnProperty(key)) { try { const data = JSON.parse(localStorage.getItem(key)); entries.push({ key: key, timestamp: data.timestamp || 0, size: (key.length + localStorage.getItem(key).length) * 2 }); } catch (e) { // Not JSON, skip } } }
// Sort by timestamp (oldest first) entries.sort((a, b) => a.timestamp - b.timestamp);
// Remove entries until we've freed enough space let freed = 0; for (const entry of entries) { if (freed >= bytesToFree) break; localStorage.removeItem(entry.key); freed += entry.size; console.log(`Removed old entry: ${entry.key}`); }
return freed;}Best Practices I Now Follow
After this incident, I implemented these rules for my team:
- Never assume localStorage works - Always check availability first
- Always use try-catch - Wrap every
setItem()call - Set size limits - Don’t store unlimited data without cleanup
- Have a fallback - Use IndexedDB or server-side storage for larger data
- Clear proactively - Don’t wait for quota errors to clean up
- Monitor usage - Warn users when approaching limits
Quick Checklist
Before deploying code that uses localStorage, verify:
- All
setItem()calls have try-catch blocks - There’s a fallback when storage fails (graceful degradation)
- You’re not storing large binary data (use IndexedDB instead)
- Old data gets cleaned up regularly
- - [ ] You’ve tested with full storage (I simulate this by filling localStorage in DevTools)
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments