Why localStorage Silently Fails in Safari

JS Edition

June 29, 2026

You ship a feature. It works perfectly in Chrome. You test it in Firefox. Still fine. Then someone files a bug that their settings aren’t saving, their preferences reset on every visit, the thing you built just doesn’t work.

You dig into it. No JavaScript errors. No console warnings. The code looks right. And then you notice: they’re on Safari.

This is one of those bugs that doesn’t announce itself. It just quietly does nothing.


The obvious case first

If a user is browsing in Private mode on Safari, localStorage is available but the storage quota is set to zero. Any write silently succeeds with no error thrown; but nothing actually gets stored. The next read returns null.

localStorage.setItem('theme', 'dark');\nlocalStorage.getItem('theme'); // null

That’s not a typo. The write appeared to work. The read returns nothing. Safari doesn’t throw. It doesn’t warn. It just discards your data.

This is the most common version of the bug and the one most developers eventually learn about. But it’s not the only way localStorage fails in Safari.


The ITP problem

Safari has a feature called Intelligent Tracking Prevention. It was introduced to limit cross-site tracking; specifically to stop advertisers from following users around the web using cookies and storage.

The side effect is that localStorage gets partitioned and, in some configurations, cleared automatically.

If your site is loaded inside an iframe on another domain — which happens more than you’d think, in embedded widgets, payment flows, third-party integrations — Safari may block localStorage access entirely for that embedded context. The write throws a SecurityError in this case, but if you’re not wrapping your localStorage calls in try/catch, that error surfaces as an uncaught exception and your feature silently stops working.

// In a cross-origin iframe on Safari\nlocalStorage.setItem('key', 'value'); // SecurityError: The operation is insecure.

Outside iframes, ITP can still clear localStorage for sites the user hasn’t interacted with directly in the last seven days. If your app is something users visit occasionally — a tool, a dashboard, a utility — their stored preferences can just vanish between visits.


The storage quota exception

Even outside Private mode, Safari has historically had stricter storage quotas than Chrome or Firefox. When you exceed the quota, setItem throws a QuotaExceededError.

This one at least throws; but most code doesn’t catch it:

// This can throw on Safari\nlocalStorage.setItem('bigKey', hugeString);

If you’re storing anything non-trivial; cached API responses, serialised state, user-generated content; you can hit this without realising it, especially on older iOS devices with less available storage.


How to write localStorage code that actually works

The fix is a wrapper. Not glamorous, but necessary:

const storage = {\n  get(key) {\n    try {\n      return localStorage.getItem(key);\n    } catch {\n      return null;\n    }\n  },\n\n  set(key, value) {\n    try {\n      localStorage.setItem(key, value);\n      return true;\n    } catch {\n      return false;\n    }\n  },\n\n  remove(key) {\n    try {\n      localStorage.removeItem(key);\n    } catch {\n      // ignore\n    }\n  }\n};

This handles the SecurityError from iframe contexts and the QuotaExceededError from full storage. It doesn’t fix Private mode; nothing can, by design — but it stops those cases from taking down your whole feature.

If you want to detect Private mode specifically and handle it gracefully:

function isLocalStorageAvailable() {\n  try {\n    const test = '__storage_test__';\n    localStorage.setItem(test, test);\n    localStorage.removeItem(test);\n    return true;\n  } catch {\n    return false;\n  }\n}

Run this once on app load. If it returns false, fall back to an in-memory store or just accept that persistence won’t work for this session. Show the user a message if it matters. Don’t silently fail.


Why Safari specifically

Chrome and Firefox also have Private modes, and they also restrict localStorage in private browsing. But Chrome throws a QuotaExceededError on write attempts in Incognito — which at least means your try/catch catches it. Safari’s zero-quota approach, where writes appear to succeed but reads return null, is uniquely deceptive.

The ITP behaviour is also Safari-specific. No other major browser implements automatic storage clearing based on interaction recency at the same level of aggression.

Apple’s position is that this is privacy-protecting behaviour, not a bug. Which is fine — they’re not wrong about the privacy part. But it means the behaviour isn’t going away. If anything, it’s gotten stricter with each Safari release.


The broader point

localStorage looks like a simple key-value store. In practice it’s a browser API with a different failure surface on every engine, and Safari’s failure surface is the most surprising because it doesn’t always fail loudly.

The rule is simple: never call localStorage without a try/catch, and never assume a successful setItem means the data is actually there. Always verify with a getItem if the stored value is load-bearing for your feature.

It takes twenty lines of wrapper code to make localStorage reliable. Write it once, put it in a utils file, and stop assuming the browser will be honest with you.


Thanks for reading Under The Hood! Subscribe for free to receive new posts and support my work.

Leave a comment