Lesson 66 of 80
Two tiny synchronous string stores and one real async database — picking the wrong one is how apps end up JSON.stringify-ing megabytes on the main thread.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The browser gives you three separate places to keep data on the user's machine, and they solve different problems.
localStorage and sessionStorage are the simple ones: both are key-value stores of strings only, both are synchronous (every read and write blocks the main thread), and the only difference between them is lifetime and scope. localStorage persists until something explicitly clears it — closing the tab, the browser, restarting the machine, none of that touches it. sessionStorage dies with the tab: close it, and it's gone; open the same site in a new tab and you get an empty store, even though it's the same origin.
IndexedDB is a different animal entirely — an actual transactional database running in the browser, storing structured JavaScript values (not just strings), indexed for fast lookup, and asynchronous by design so large reads and writes don't freeze the page.
Say you're saving a comment draft as the user types, a theme preference, and a large offline cache of API responses. Each belongs somewhere different:
// Theme preference: small, should survive browser restarts
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme"); // "dark" — always a string
// Draft for THIS tab only — a second tab shouldn't clobber it
sessionStorage.setItem("draftId", "42");
// Objects need JSON.stringify going in, JSON.parse coming out — neither
// storage knows what an object is
const prefs = { theme: "dark", fontSize: 14 };
localStorage.setItem("prefs", JSON.stringify(prefs));
const restored = JSON.parse(localStorage.getItem("prefs"));
// A real offline cache of structured records — this is IndexedDB's job
const request = indexedDB.open("app-cache", 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
db.createObjectStore("responses", { keyPath: "url" });
};
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction("responses", "readwrite");
tx.objectStore("responses").put({ url: "/api/user", data: { id: 1 }, cachedAt: Date.now() });
};
Notice localStorage.getItem returns a string even though we stored an object — it silently coerced it via JSON.stringify unless you did that yourself, and forgetting the matching JSON.parse gets you the literal text "[object Object]" back, not an error.
localStorage.setItem call blocks the main thread until the write completes. For small values this is invisible; for large ones it can cause jank. IndexedDB's async API exists specifically to avoid this.sessionStorage is per-tab, not per-window or per-site. Duplicating a tab copies its sessionStorage; opening the site fresh in a new tab does not. This trips people up when testing "why isn't my session data there" in a second tab.localStorage/sessionStorage are typically capped around 5-10MB per origin; exceeding it throws a QuotaExceededError. IndexedDB's limit is a much larger share of available disk space, which is part of why it's the right tool for real datasets.This is the same tradeoff that shows up everywhere in engineering: a synchronous string dictionary is trivially easy to use but doesn't scale past small, simple data, while an asynchronous structured database has more ceremony but scales to real workloads. The event loop and microtask/macrotask lessons in this track explain why an async API like IndexedDB never blocks rendering the way a synchronous one can.