Introduction
Safari on iOS has strict localStorage limitations that cause QuotaExceededError when writing data, particularly in Private Browsing mode where localStorage is completely disabled. The error manifests as:
Uncaught QuotaExceededError: The quota has been exceeded.
at localStorage.setItem('app-state', JSON.stringify(state))This breaks applications that rely on localStorage for session state, preferences, or cached data, causing crashes on iOS devices.
Symptoms
- Application crashes on Safari iOS with QuotaExceededError
- Features work on desktop browsers but fail on iPhone/iPad
- Private Browsing mode in Safari completely blocks localStorage writes
- Error occurs even with small amounts of data (less than 1MB)
typeof localStoragereturns"object"but anysetItemcall throws
Common Causes
- Safari Private Browsing mode sets localStorage quota to 0 bytes
- Safari iOS enforces a 5MB per-origin limit (vs 10MB in Chrome)
- Storing large JSON objects or base64-encoded images in localStorage
- Accumulated data from long-term usage exceeding the quota over time
- Multiple tabs writing to the same key simultaneously
Step-by-Step Fix
- 1.Detect localStorage availability before using it:
- 2.```javascript
- 3.function isLocalStorageAvailable() {
- 4.try {
- 5.const testKey = '__storage_test__';
- 6.localStorage.setItem(testKey, testKey);
- 7.localStorage.removeItem(testKey);
- 8.return true;
- 9.} catch (e) {
- 10.return false;
- 11.}
- 12.}
- 13.
` - 14.Implement a storage abstraction with fallback:
- 15.```javascript
- 16.class SafeStorage {
- 17.constructor() {
- 18.this.available = isLocalStorageAvailable();
- 19.this.memory = new Map();
- 20.}
setItem(key, value) { if (this.available) { try { localStorage.setItem(key, value); } catch (e) { if (e.name === 'QuotaExceededError') { this.clearOldestItems(); localStorage.setItem(key, value); } } } else { this.memory.set(key, value); } }
getItem(key) { if (this.available) { return localStorage.getItem(key); } return this.memory.get(key) || null; }
clearOldestItems() { // Remove oldest 25% of items const keys = Object.keys(localStorage); const removeCount = Math.floor(keys.length * 0.25); for (let i = 0; i < removeCount; i++) { localStorage.removeItem(keys[i]); } } }
const storage = new SafeStorage(); ```
- 1.Use IndexedDB for larger data as an alternative:
- 2.```javascript
- 3.function openDatabase() {
- 4.return new Promise((resolve, reject) => {
- 5.const request = indexedDB.open('app-db', 1);
- 6.request.onupgradeneeded = (e) => {
- 7.e.target.result.createObjectStore('cache');
- 8.};
- 9.request.onsuccess = (e) => resolve(e.target.result);
- 10.request.onerror = (e) => reject(e.target.error);
- 11.});
- 12.}
async function storeLargeData(key, value) { const db = await openDatabase(); const tx = db.transaction('cache', 'readwrite'); tx.objectStore('cache').put(value, key); return tx.complete; } ```
- 1.Compress data before storing to reduce localStorage usage:
- 2.```javascript
- 3.import { compress, decompress } from 'lz-string';
storage.setItem('app-state', compress(JSON.stringify(appState))); const state = JSON.parse(decompress(storage.getItem('app-state'))); ```
Prevention
- Never assume localStorage is available - always wrap access in try/catch
- Set a per-key storage budget (e.g., 1MB max per key) and enforce it
- Use IndexedDB for data exceeding a few hundred kilobytes
- Implement automatic cleanup of stale cached data
- Monitor storage usage:
JSON.stringify(localStorage).lengthto track total bytes used - Test your application in Safari Private Browsing mode during QA
- Consider using session-based storage (sessionStorage) for transient data instead of localStorage