The call localStorage.setItem('user', { id: 1 }) runs without throwing. What is actually stored, and what does localStorage.getItem('missing') return for a key that was never written?
answer
- the store holds text and nothing else
- non-strings are converted, not rejected
- the default conversion of an object
- absent key has its own return value
- parsing that value does not throw
basics
~20 sWeb Storage keeps strings only, so setItem converts the object with the ordinary string conversion and stores the literal text "[object Object]". Reading a key that was never written returns null, not undefined, so structured data must be serialised and parsed explicitly.
solid answer
~40 s`Storage` is defined to hold string keys and string values, and `setItem` does not reject other types — it stringifies them. An object becomes `"[object Object]"`, the number `42` becomes `"42"`, `null` becomes `"null"`, and the original value is unrecoverable. So anything structured has to be serialised on the way in and parsed on the way out. Reads are equally sharp-edged: `getItem` returns `null` for an absent key, never `undefined`, which matters because `JSON.parse(null)` quietly returns `null` instead of throwing, hiding the fact that nothing was there. A stored value that is *not* valid JSON does throw a `SyntaxError`, so real read paths guard the parse and treat a corrupt entry as absent rather than letting it break page startup.
code
javascript · 11 lineslocalStorage.setItem('user', { id: 1 });
console.log(localStorage.getItem('user')); // "[object Object]"
localStorage.setItem('n', 42);
console.log(typeof localStorage.getItem('n')); // "string"
console.log(localStorage.getItem('missing')); // null
console.log(JSON.parse(localStorage.getItem('missing'))); // null, no error
localStorage.setItem('user', JSON.stringify({ id: 1 }));
console.log(JSON.parse(localStorage.getItem('user')).id); // 1go deeper
Be ready to say that only strings are stored, so objects must be serialised before writing and parsed after reading, and that a missing key reads back as null.
Explain that setItem performs a silent string conversion rather than rejecting the value, name the exact result for an object and an array, and describe why a null read does not make a parse throw.
Demonstrate a production read path: guard the parse, remove or ignore an unreadable entry, supply a default, and validate that the parsed shape matches what today's code expects rather than trusting data written by an older release.
Own persisted client state as a format with versions: how entries are namespaced, how a shape change is migrated or discarded across deploys, and what the policy is for data that must not be written to the client at all.
## The interface says string, and it means it The `Storage` interface stores a map of string keys to string values. Both `setItem` arguments go through the normal JavaScript string conversion before they are stored — the same conversion `String(value)` performs. Nothing is rejected and nothing is validated, which is why the call in the question runs happily and produces a value that is useless: ```js localStorage.setItem('user', { id: 1 }); localStorage.getItem('user'); // "[object Object]" ``` The object was converted by its default `toString`, and the property is gone. The same silent flattening happens to every non-string type: ```js localStorage.setItem('n', 42); // stored as "42" localStorage.setItem('ok', true); // stored as "true" localStorage.setItem('nothing', null); // stored as "null" localStorage.setItem('list', [1, 2]); // stored as "1,2" typeof localStorage.getItem('n'); // "string" ``` Arrays are the nastiest of these, because `[1, 2].toString()` is `"1,2"` — plausible-looking text that a reader may mistake for something structured. Keys are converted too, so `setItem(1, 'x')` and `setItem('1', 'x')` write the same entry. ## Serialise on write, parse on read The conventional fix is to serialise structured values yourself before writing and parse them after reading. That makes the round trip explicit, and it also makes the failure modes explicit, which is the part teams skip. ## getItem returns null, not undefined The spec says a read of a key that is not present returns `null`. That is a distinct value from `undefined`, and it matters in three places. First, truthiness checks: `if (localStorage.getItem('flag'))` is `false` both when the key is missing *and* when the stored string is `""`, because the empty string is falsy. If "present but empty" is meaningful in your data, compare against `null` explicitly, or use `key in localStorage`-style checks carefully — note that `Storage` also supports property access (`localStorage.flag`), which returns `undefined` for a missing key rather than `null`, so the two access styles disagree. Prefer the methods and stay consistent. Second, defaults: `const raw = localStorage.getItem('theme') ?? 'light'` works because `??` treats `null` as missing, whereas destructuring defaults (which only fire on `undefined`) do not. Third, and most commonly, the parse step: ```js JSON.parse(localStorage.getItem('missing')); // null — no error ``` That is not a special case in the storage API. The parse argument is converted to a string, `null` becomes the text `"null"`, and `"null"` is valid JSON for the value `null`. So the pattern `const state = JSON.parse(localStorage.getItem(key))` silently yields `null` when the key was never written, and code that then reaches for `state.something` throws a `TypeError` far away from the actual cause. ## Corrupt values do throw The opposite case is a key that holds a string which is not valid JSON — written by an older version of your app, by a browser extension, by a hand edit in devtools, or by a half-finished write. Parsing that throws a `SyntaxError` synchronously. Because the read usually happens during startup or during hydration of a store, an unguarded parse turns one bad key into a blank page. The defensive shape is small and worth writing once: ```js function readJson(key, fallback = null) { const raw = localStorage.getItem(key); if (raw === null) return fallback; try { return JSON.parse(raw); } catch { localStorage.removeItem(key); return fallback; } } ``` Dropping the unreadable key is deliberate: it stops the same failure recurring on every load and frees the space it occupied. ## Consequences for what you persist Because the medium is text and the conversion is yours, persisted values are effectively a versioned format you own. Anything whose runtime type does not survive being written as text and read back has to be reconstructed by hand on read. Shapes change across releases, so a stored entry written months ago by an older build may not match what today's code expects — validating the parsed value against the shape you need, rather than trusting it, is what separates a robust read path from one that throws on a user whose browser has been open since the last deploy. Also note the cost: serialising and parsing happen on the main thread, synchronously, every time. For a small preference that is irrelevant; for a large state blob it is real work in the critical path. ## What interviewers are listening for The short answer — "strings only, so the object is stringified and you get `[object Object]`; missing keys read as `null`" — is table stakes. The stronger answer adds that the conversion is silent, that `null` from a missing key does not make a parse throw, and that a read path in production guards the parse and validates the result.
- Your read path is `const state = JSON.parse(localStorage.getItem('state'))` and a user reports a TypeError deep in the app. What is the likely chain?The key was never written or was cleared, `getItem` returned `null`, and `JSON.parse(null)` parsed the text `"null"` and returned `null` without complaining. `state` is therefore `null`, and the first property access on it throws far from the real cause. Check for `null` before parsing and return an explicit default.
- What should happen when a stored value fails to parse?Treat the entry as absent: catch the `SyntaxError`, fall back to a default, and remove the key so the same failure does not repeat on every load. An unguarded parse during startup turns one corrupt entry — from an old build, an extension, or a manual edit — into a blank page for that user until they clear site data.
- Is there any difference between localStorage.getItem('x') and localStorage.x?Both read the same entry, but they disagree on absence: the method returns `null`, while the property access returns `undefined`. Property access also collides with the interface's own members — a key named `length` or `clear` is shadowed by the built-in property or method. Use the methods consistently for predictable behaviour.
saying these in an interview costs you the question
- Thinks setItem stores objects and getItem returns them
- Says getItem returns undefined for a missing key
- Assumes JSON.parse throws when the key is absent
- Believes numbers keep their type through a round trip
- Parses stored values with no error handling at all