Under what conditions can localStorage.setItem() throw — or even the plain expression window.localStorage throw before you call any method — and how should production code guard against it?
answer
- two failures, not one
- the budget is shared with every script on the origin
- the property access itself can raise
- a feature check that touches it is already unsafe
- probe with a real write, then remove it
basics
~20 sWrites throw a QuotaExceededError once the origin passes its limit, commonly around 5 MB. Access itself can throw a SecurityError when site data is blocked, for example in an embedded frame under restrictive settings. Feature-detect with a try/catch write probe and degrade instead of crashing.
solid answer
~50 sThere are two distinct failure modes. The first is quota: Web Storage gives each origin a modest budget — commonly about 5 MB, browser-dependent — and a `setItem` that would exceed it throws a `DOMException` named `QuotaExceededError` with nothing written. The second is availability: when the user or the environment blocks site data, touching the `localStorage` property itself throws a `SecurityError`, so a bare `typeof localStorage` guard is not enough and the throw happens before any method call. Historically some private-browsing modes reported a zero quota so every write threw; current browsers generally give a private session its own ephemeral store instead. The robust pattern is a one-time probe — write and remove a throwaway key inside `try/catch` — cache the boolean, and fall back to an in-memory store so a blocked or full browser degrades rather than white-screens.
code
javascript · 25 linesconst memory = new Map();
const usable = (() => {
try {
const probe = '__probe__';
window.localStorage.setItem(probe, probe);
window.localStorage.removeItem(probe);
return true;
} catch {
return false; // blocked site data, or a zero quota
}
})();
export function write(key, value) {
if (!usable) return memory.set(key, value), true;
try {
window.localStorage.setItem(key, value);
return true;
} catch (error) {
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
return false; // caller decides: evict, warn, or drop
}
throw error;
}
}go deeper
Know that a write can fail — the browser gives each site only a few megabytes — and that the failure is an exception, so a write of anything sizeable belongs inside try/catch.
Name the two failures precisely: QuotaExceededError from a write past the origin limit, and a SecurityError from touching the property when site data is blocked. Explain why a typeof check does not protect you.
Describe the resilient wrapper you would ship: a one-time round-trip probe, a memory fallback, namespaced keys, a narrow quota branch that evicts and retries once, and a user-visible outcome when persistence genuinely is not available.
Own the failure policy across the product: which features may depend on client persistence at all, what the degraded experience is when it is unavailable, and how the origin's shared budget is allocated and audited among first-party code and third-party scripts.
## Two different failures, two different fixes Code that treats storage as infallible is common and fragile. There are two independent ways it fails, and they need different handling. ## Failure one: the origin is out of room Web Storage is capped per origin. The figure is not specified — around 5 MB is the usual ballpark, and browsers differ on the exact number and on whether key names count toward it. Exceeding it makes `setItem` throw a `DOMException` whose `name` is `QuotaExceededError`. The write does not partially succeed: the entry is not stored, and the previous value (if any) is untouched. ```js try { localStorage.setItem('appState', bigString); } catch (error) { if (error instanceof DOMException && error.name === 'QuotaExceededError') { evictOldEntries(); } else { throw error; } } ``` Branch on `name`, not on the message or a numeric code — messages are browser-specific prose and legacy codes differ between engines. What to do in the handler is a product decision: drop the least valuable cached entries and retry once, downgrade to memory-only for this session, or tell the user their offline draft could not be saved. Silently swallowing the error is the one option that reliably produces a bug report about "my work disappeared". Quota failures are also *other people's* problem in a shared origin. Every script on the origin — your app, an analytics tag, a chat widget, a browser extension injecting into the page — writes into the same 5 MB. Your write can fail because of theirs, so treat the budget as shared and namespace your keys so you can find and evict your own. ## Failure two: storage is not available at all The sharper failure is that the getter throws. When site data is blocked — a browser setting that blocks cookies and site data, an enterprise policy, or an embedded third-party frame whose access is denied — the property access `window.localStorage` itself raises a `SecurityError` `DOMException`. That means the usual defensive shapes are wrong: ```js if (typeof localStorage !== 'undefined') { /* already threw on the access */ } if ('localStorage' in window) { /* the property exists; using it still throws */ } ``` Both touch the property, so both can throw before your guard has a chance to run. The only reliable feature detection is a `try/catch` around an actual round trip, because a browser can also expose the object while silently refusing to persist: ```js function storageAvailable() { try { const probe = '__probe__'; window.localStorage.setItem(probe, probe); window.localStorage.removeItem(probe); return true; } catch { return false; } } ``` Run that once at startup, cache the result, and route every access through a wrapper that falls back to an in-memory `Map` when it is false. The app then loses persistence — which is unavoidable — without losing functionality, which is not. ## Private browsing, then and now Private/incognito modes are the case candidates usually reach for. Historically, some browsers exposed `localStorage` in a private window but reported a zero quota, so every single `setItem` threw `QuotaExceededError` — a spectacular way for an app to break for a minority of users. Current mainstream browsers instead give the private session its own ephemeral store that behaves normally and is discarded when the session ends. The practical lesson survives the behaviour change: never assume a write succeeds, and never assume data written in one session will still be there in the next. ## Reads can be surprising too Even where writing works, the data may not be what you left. Another tab or another script may have removed or overwritten your key; the user may have cleared site data; the browser may have evicted the origin. Read paths should treat every entry as optional and every value as untrusted text: check for `null`, guard the parse, and validate the shape before use. ## The shape of a resilient wrapper Put all of this behind one module: an availability probe run once, a memory fallback, namespaced keys, a `QuotaExceededError` branch that evicts and retries at most once, and JSON helpers that never throw out to the caller. It is perhaps thirty lines, and it converts three separate classes of white-screen into a degraded but working page. Being able to describe that module — and to say precisely which exception name belongs to which failure — is what the question is really testing.
- Why is `if ('localStorage' in window)` not a safe feature detection?It answers the wrong question. The property can be present and still throw a `SecurityError` on access when site data is blocked, and an environment can expose a working-looking object that refuses to persist. Only an actual round trip inside `try/catch` — write a throwaway key, read or remove it — establishes that storage is both reachable and usable.
- How would you distinguish a quota failure from any other exception thrown by setItem?Check `error instanceof DOMException && error.name === 'QuotaExceededError'` and rethrow anything else. Do not match on the message, which is browser-specific prose, or on legacy numeric codes, which differ between engines. Keeping the branch narrow means a genuinely unexpected error still surfaces instead of being swallowed by a catch-all that assumes every failure is a full store.
- Your app's writes start failing even though your own data is tiny. What is going on?The quota belongs to the origin, not to your code. Every script running there — analytics, a support widget, an experimentation SDK, an injected extension — shares the same few megabytes, so someone else's growth can exhaust it. Namespace your keys with a stable prefix so you can audit and evict your own entries, and keep a memory fallback for when the space genuinely is not available.
saying these in an interview costs you the question
- Assumes setItem never throws and wraps nothing
- Feature-detects with typeof or an `in` check on window
- Matches on the exception message instead of its name
- Believes the quota is per script rather than per origin
- Catches the error and continues as if the write succeeded