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.

5 min read

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:

javascriptjavascript
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:

texttext
dark

This 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:

javascriptjavascript
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:

texttext
[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:

TypeSurvives?Notes
String, Number, BooleanYesInfinity and NaN become null
nullYes
Array, plain objectYesNested structures work
DatePartiallyConverted to ISO string; restore with new Date()
undefinedNoIgnored in objects, becomes null in arrays
Function, SymbolNoSilently dropped
Map, SetNoBecomes an empty object {}
BigIntNoThrows 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:

javascriptjavascript
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:

texttext
light

This 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:

javascriptjavascript
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:

javascriptjavascript
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:

texttext
true

The 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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

texttext
1.2

The 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.

Quick reference

PatternCode
Save an objectlocalStorage.setItem("key", JSON.stringify(obj))
Read an objectJSON.parse(localStorage.getItem("key"))
Safe read with fallbacktry/catch with a default value
Handle DatesSave as toISOString(), restore with new Date()
Check quota before writeWrap setItem in try/catch
Rune AI

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.
RunePowered by Rune AI

Frequently Asked Questions

Can localStorage store Date objects?

Not directly. Dates must be converted to strings with toISOString() before saving. When reading, reconstruct them with new Date(storedString).

What happens if JSON.parse fails on stored data?

JSON.parse throws a SyntaxError if the string is not valid JSON. Always wrap JSON.parse calls in a try/catch block to handle corrupted or unexpected data gracefully.

How do I store functions in localStorage?

You cannot. localStorage stores strings, and JSON.stringify ignores functions. Store configuration data and function names instead, then map them back to functions in your code when reading.

Can I store binary data or files in localStorage?

Technically yes by base64-encoding the data into a string, but this is a bad idea. localStorage has a 5 MB limit and is synchronous. Use IndexedDB for files and binary data.

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.