Storing Complex Objects in JS localStorage Guide
Learn how to safely store and retrieve JavaScript objects, arrays, and nested data in localStorage using JSON serialization with proper error handling.
localStorage can only store strings. If you try to save a JavaScript object directly, the browser calls toString on it and stores the unhelpful string "[object Object]". To store real objects, arrays, and nested data, you need JSON.stringify before writing and JSON.parse after reading.
Here is the basic pattern. It works for any JSON-safe value:
const settings = { theme: "dark", fontSize: 16, notifications: true };
localStorage.setItem("appSettings", JSON.stringify(settings));
const saved = JSON.parse(localStorage.getItem("appSettings"));
console.log(saved.theme);The settings object is converted to a JSON string before storage, then parsed back into a real JavaScript object. Running this code prints the theme value:
darkThis same pattern works for arrays and nested structures of any depth.
Why plain setItem fails on objects
The problem shows up the moment you pass an object straight into setItem instead of converting it first. Run this quick test to see what actually gets stored:
const user = { name: "Rune", level: 5 };
localStorage.setItem("user", user);
console.log(localStorage.getItem("user"));Running this code shows the problem immediately. Instead of the real object you passed to setItem, all you get back is a useless string representation of the object's type:
[object Object]Every JavaScript object has a toString method. localStorage calls it, and the default implementation returns "[object Object]". The actual data is lost forever.
What survives JSON serialization
JSON supports a limited set of types. Anything outside this set is lost, converted, or ignored during the round trip:
| Type | Survives? | Notes |
|---|---|---|
| String, Number, Boolean | Yes | Infinity and NaN become null |
| null | Yes | |
| Array, plain object | Yes | Nested structures work |
| Date | Partially | Converted to ISO string; restore with new Date() |
| undefined | No | Ignored in objects, becomes null in arrays |
| Function, Symbol | No | Silently dropped |
| Map, Set | No | Becomes an empty object {} |
| BigInt | No | Throws TypeError |
The biggest practical surprises are Date objects and undefined values. A Date becomes a string that needs explicit reconstruction. Fields with the value undefined disappear entirely, so checking for their existence after a round trip will fail.
Safe patterns for reading stored objects
The most common bug when reading stored objects is calling JSON.parse on null. If a key has never been written, getItem returns null, and accessing a property on null crashes with a TypeError. The safe pattern wraps the read in a fallback:
function readSettings() {
try {
const raw = localStorage.getItem("appSettings");
return raw ? JSON.parse(raw) : { theme: "light", fontSize: 14 };
} catch {
return { theme: "light", fontSize: 14 };
}
}
const settings = readSettings();
console.log(settings.theme);The readSettings function always returns a usable object no matter what state localStorage is in. When nothing has been saved yet, it uses the default fallback values:
lightThis handles three failure cases in one function: the key does not exist, the stored string is corrupted, and the browser blocked localStorage access. In all cases, the caller gets a usable default object.
Handling dates and special types
If your stored objects include Dates, convert them explicitly when reading. The same principle applies to any non-JSON type: store a representation you can reconstruct, and rebuild it when reading.
Save dates as ISO strings, which JSON can represent:
const session = { userId: "abc", startedAt: new Date().toISOString() };
localStorage.setItem("session", JSON.stringify(session));The startedAt field is now a string inside the stored JSON. When reading it back, you need to explicitly reconstruct the Date object from that string:
const raw = localStorage.getItem("session");
if (raw) {
const parsed = JSON.parse(raw);
parsed.startedAt = new Date(parsed.startedAt);
console.log(parsed.startedAt instanceof Date);
}The restoration converts the ISO string back to a real Date object. Here is proof that it is now a proper Date:
trueThe startedAt field is a real Date object after restoration. Without the explicit new Date call, it would remain a plain string.
A reusable storage utility
Instead of scattering JSON serialization throughout your codebase, wrap it once into a small helper. Start with the save method, which serializes and writes in one call:
const storage = {
save(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
return false;
}
},
};The save method wraps localStorage.setItem with JSON.stringify and returns a boolean indicating success or failure. This single method prevents the three most common storage bugs at once: forgetting to serialize before writing, not catching quota errors that crash the page, and not knowing whether a write actually succeeded.
Now add a matching load method that safely reads and parses stored data back into a JavaScript object:
storage.load = function (key, fallback = null) {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch {
return fallback;
}
};The load method tries to parse the stored JSON and returns the fallback when anything goes wrong. With these two methods defined, you can store and retrieve objects safely:
storage.save("userPrefs", { theme: "dark", zoom: 1.2 });
const prefs = storage.load("userPrefs", { theme: "light", zoom: 1.0 });
console.log(prefs.zoom);The utility stores the preferences object under the userPrefs key and reads it back through the load method. The zoom value confirms the data survived the round trip:
1.2The save method returns true or false so callers know if the write succeeded. The load method always returns a usable value, never null, by using the fallback parameter. This small wrapper prevents the most common localStorage bugs across your entire application.
Common mistakes
- Saving an object without JSON.stringify. You get "[object Object]" and lose the data.
- Parsing null and then accessing a property. Always check that getItem returned a string before calling JSON.parse.
- Assuming JSON.parse can never fail. Corrupted data, manual DevTools edits, and storage migrations can all produce invalid JSON.
- Storing deeply nested data that grows over time. localStorage is synchronous and large objects freeze the UI during serialization.
- Forgetting that undefined values and functions disappear from stored objects. Design your storage schema with only JSON-safe types.
Related articles
- JS localStorage API Guide: A Complete Tutorial -- the full localStorage API reference
- JS sessionStorage API Guide: Complete Tutorial -- temporary storage scoped to a single tab
- JSON Parse and JSON Stringify in JavaScript Guide -- deeper coverage of JSON serialization
Quick reference
| Pattern | Code |
|---|---|
| Save an object | localStorage.setItem("key", JSON.stringify(obj)) |
| Read an object | JSON.parse(localStorage.getItem("key")) |
| Safe read with fallback | try/catch with a default value |
| Handle Dates | Save as toISOString(), restore with new Date() |
| Check quota before write | Wrap setItem in try/catch |
Rune AI
Key Insights
- localStorage only stores strings. Use JSON.stringify to convert objects before saving.
- Use JSON.parse to convert stored strings back into objects when reading.
- Always wrap JSON.parse in try/catch to handle corrupted or missing data.
- Dates, Maps, Sets, and functions need special handling since they do not survive JSON round trips.
- Create a small storage helper module that all your code shares.
Frequently Asked Questions
Can localStorage store Date objects?
What happens if JSON.parse fails on stored data?
How do I store functions in localStorage?
Can I store binary data or files in localStorage?
Conclusion
Storing objects in localStorage comes down to two steps: JSON.stringify before writing and JSON.parse after reading. The real skill is handling the edge cases: corrupted data, missing keys, deeply nested objects with Dates or special types, and quota errors. Build a small wrapper once and reuse it everywhere.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.