Storing Complex Objects in JS LocalStorage Guide
A complete guide to storing complex objects in JavaScript LocalStorage. Covers JSON serialization, Date and RegExp reviving, Map and Set handling, circular reference detection, TTL-based expiry patterns, versioned storage with migrations, compression with LZString, and building a typed storage wrapper with schema validation.
LocalStorage only stores strings. Storing objects, arrays, Dates, Maps, Sets, and deeply nested structures requires serialization strategies that preserve type information and handle edge cases. This guide covers every pattern you need.
Basic JSON Serialization
// Store an object
const user = {
name: "Parth",
age: 28,
roles: ["admin", "editor"],
active: true,
};
localStorage.setItem("user", JSON.stringify(user));
// Retrieve and parse
const stored = JSON.parse(localStorage.getItem("user"));
console.log(stored.name); // "Parth"
console.log(stored.roles); // ["admin", "editor"]For the full localStorage API reference, see JS localStorage API guide: a complete tutorial.
Handling Special Types
Dates
JSON.stringify converts Dates to ISO strings but JSON.parse returns them as strings, not Date objects:
const event = {
title: "Conference",
startDate: new Date("2026-06-15T09:00:00Z"),
endDate: new Date("2026-06-15T17:00:00Z"),
};
localStorage.setItem("event", JSON.stringify(event));
// Without reviver: dates are strings
const raw = JSON.parse(localStorage.getItem("event"));
console.log(typeof raw.startDate); // "string"
// With reviver: dates are restored
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
function dateReviver(key, value) {
if (typeof value === "string" && ISO_DATE_RE.test(value)) {
return new Date(value);
}
return value;
}
const parsed = JSON.parse(localStorage.getItem("event"), dateReviver);
console.log(parsed.startDate instanceof Date); // true
console.log(parsed.startDate.toLocaleDateString()); // "6/15/2026"Maps and Sets
// Custom replacer and reviver for Map and Set
function advancedReplacer(key, value) {
if (value instanceof Map) {
return { __type: "Map", entries: [...value.entries()] };
}
if (value instanceof Set) {
return { __type: "Set", values: [...value.values()] };
}
if (value instanceof Date) {
return { __type: "Date", iso: value.toISOString() };
}
if (value instanceof RegExp) {
return { __type: "RegExp", source: value.source, flags: value.flags };
}
return value;
}
function advancedReviver(key, value) {
if (value && typeof value === "object" && value.__type) {
switch (value.__type) {
case "Map":
return new Map(value.entries);
case "Set":
return new Set(value.values);
case "Date":
return new Date(value.iso);
case "RegExp":
return new RegExp(value.source, value.flags);
}
}
return value;
}
// Usage
const data = {
tags: new Set(["javascript", "tutorial", "storage"]),
metadata: new Map([
["version", 2],
["author", "Parth"],
]),
created: new Date(),
};
localStorage.setItem("data", JSON.stringify(data, advancedReplacer));
const restored = JSON.parse(localStorage.getItem("data"), advancedReviver);
console.log(restored.tags instanceof Set); // true
console.log(restored.metadata instanceof Map); // true
console.log(restored.created instanceof Date); // trueSerialization Support Matrix
| Type | JSON.stringify | Needs Replacer | Needs Reviver |
|---|---|---|---|
| String, Number, Boolean | Yes | No | No |
| Array, Plain Object | Yes | No | No |
| Date | ISO string | Optional | Yes |
| Map | {} (lost) | Yes | Yes |
| Set | {} (lost) | Yes | Yes |
| RegExp | {} (lost) | Yes | Yes |
| Function | Omitted | N/A | N/A |
undefined | Omitted | N/A | N/A |
| Symbol | Omitted | N/A | N/A |
Circular Reference Detection
function safeStringify(obj, replacer = null, space = 0) {
const seen = new WeakSet();
return JSON.stringify(obj, function (key, value) {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);
}
if (replacer) {
return replacer.call(this, key, value);
}
return value;
}, space);
}
// Safe with circular references
const a = { name: "A" };
const b = { name: "B", ref: a };
a.ref = b; // circular
const json = safeStringify(a);
console.log(json); // {"name":"A","ref":{"name":"B","ref":"[Circular]"}}TTL-Based Expiry Pattern
class ExpiringStorage {
static set(key, value, ttlMs) {
const entry = {
value,
expiresAt: Date.now() + ttlMs,
storedAt: Date.now(),
};
localStorage.setItem(key, JSON.stringify(entry, advancedReplacer));
}
static get(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
const entry = JSON.parse(raw, advancedReviver);
if (entry.expiresAt && Date.now() > entry.expiresAt) {
localStorage.removeItem(key);
return null;
}
return entry.value;
} catch {
return null;
}
}
static getRemainingTTL(key) {
const raw = localStorage.getItem(key);
if (!raw) return 0;
try {
const entry = JSON.parse(raw);
const remaining = entry.expiresAt - Date.now();
return remaining > 0 ? remaining : 0;
} catch {
return 0;
}
}
static clearExpired() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
const entry = JSON.parse(localStorage.getItem(key));
if (entry.expiresAt && Date.now() > entry.expiresAt) {
keysToRemove.push(key);
}
} catch {
// Not an expiring entry, skip
}
}
keysToRemove.forEach((k) => localStorage.removeItem(k));
return keysToRemove.length;
}
}
// Cache API responses for 5 minutes
ExpiringStorage.set("apiData", { results: [1, 2, 3] }, 5 * 60 * 1000);
// Later...
const cached = ExpiringStorage.get("apiData"); // null if expiredVersioned Storage With Migrations
class VersionedStorage {
constructor(key, currentVersion, migrations) {
this.key = key;
this.currentVersion = currentVersion;
this.migrations = migrations;
}
save(data) {
const envelope = {
version: this.currentVersion,
data,
updatedAt: new Date().toISOString(),
};
localStorage.setItem(this.key, JSON.stringify(envelope, advancedReplacer));
}
load(defaultValue = null) {
const raw = localStorage.getItem(this.key);
if (!raw) return defaultValue;
try {
let envelope = JSON.parse(raw, advancedReviver);
const storedVersion = envelope.version || 1;
// Run migrations if needed
if (storedVersion < this.currentVersion) {
let data = envelope.data;
for (let v = storedVersion; v < this.currentVersion; v++) {
if (this.migrations[v]) {
data = this.migrations[v](data);
}
}
// Save migrated data
this.save(data);
return data;
}
return envelope.data;
} catch {
return defaultValue;
}
}
}
// Define migrations
const settingsStore = new VersionedStorage("settings", 3, {
1: (data) => ({ ...data, theme: data.darkMode ? "dark" : "light" }),
2: (data) => ({
...data,
notifications: { email: true, push: data.notificationsEnabled ?? true },
}),
});
// Save current version
settingsStore.save({
theme: "dark",
fontSize: 16,
language: "en",
notifications: { email: true, push: false },
});
// Load with automatic migration
const settings = settingsStore.load({ theme: "light", fontSize: 14 });Typed Storage Wrapper
class TypedStorage {
constructor(prefix = "") {
this.prefix = prefix;
}
getKey(key) {
return this.prefix ? `${this.prefix}:${key}` : key;
}
getString(key, fallback = "") {
return localStorage.getItem(this.getKey(key)) ?? fallback;
}
setString(key, value) {
localStorage.setItem(this.getKey(key), value);
}
getNumber(key, fallback = 0) {
const raw = localStorage.getItem(this.getKey(key));
if (raw === null) return fallback;
const num = Number(raw);
return Number.isNaN(num) ? fallback : num;
}
setNumber(key, value) {
localStorage.setItem(this.getKey(key), String(value));
}
getBoolean(key, fallback = false) {
const raw = localStorage.getItem(this.getKey(key));
if (raw === null) return fallback;
return raw === "true";
}
setBoolean(key, value) {
localStorage.setItem(this.getKey(key), String(value));
}
getObject(key, fallback = null) {
const raw = localStorage.getItem(this.getKey(key));
if (raw === null) return fallback;
try {
return JSON.parse(raw, advancedReviver);
} catch {
return fallback;
}
}
setObject(key, value) {
localStorage.setItem(
this.getKey(key),
JSON.stringify(value, advancedReplacer)
);
}
remove(key) {
localStorage.removeItem(this.getKey(key));
}
}
// Usage
const store = new TypedStorage("myapp");
store.setNumber("visitCount", 42);
store.setBoolean("darkMode", true);
store.setObject("profile", { name: "Parth", tags: new Set(["js", "ts"]) });
console.log(store.getNumber("visitCount")); // 42
console.log(store.getBoolean("darkMode")); // true
console.log(store.getObject("profile").tags); // Set {"js", "ts"}Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Storing objects directly | Becomes "[object Object]" | Use JSON.stringify |
| Not parsing on read | Returns a string, not object | Use JSON.parse |
| Date deserialization | Dates become strings | Use a reviver function |
| Circular references | JSON.stringify throws | Use a WeakSet-based replacer |
| Quota exceeded | Writes silently fail or throw | Wrap in try/catch, evict old data |
| No versioning | Schema changes break reads | Use versioned envelopes with migrations |
Rune AI
Key Insights
- Always serialize with JSON.stringify: Direct storage of objects produces
"[object Object]"; parse back withJSON.parseon every read - Custom replacer/reviver for special types: Dates, Maps, Sets, and RegExp lose their type through standard JSON serialization; tag them with
__typemarkers - TTL expiry for caching: Store
expiresAttimestamps alongside values to implement automatic cache invalidation without server-side logic - Versioned envelopes for schema changes: Wrap stored data with a version number and run migrations on read to handle evolving data structures
- Circular reference safety: Use a
WeakSetin a custom replacer to detect and handle circular references instead of lettingJSON.stringifythrow
Frequently Asked Questions
How do I store an array in localStorage?
Can I store class instances in localStorage?
How do I sync localStorage objects across tabs?
What happens if localStorage is full and I try to store an object?
Is there a size limit per key or only per origin?
Conclusion
Storing complex objects in localStorage requires thoughtful serialization. Use JSON.stringify/JSON.parse with custom replacer and reviver functions for Dates, Maps, Sets, and RegExp. Add circular reference detection for recursive structures, TTL patterns for cache expiry, and versioned envelopes for schema evolution. For session-scoped storage, see JS sessionStorage API guide complete tutorial. For cookie-based storage, see how to manage cookies in JS complete tutorial.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.