localStorage vs sessionStorage — When to Use Which
Photo by Markus Spiske on Unsplash
Table of Contents
They Look the Same (They're Not)
So I used localStorage for everything. Form data, user preferences, auth tokens, temporary UI state — all of it went into localStorage because I'd learned that one first and never bothered to understand why sessionStorage existed.
Then I shipped a feature where users could open the same page in multiple tabs and compare products side by side. Except every tab shared the same localStorage data. Changing the comparison list in Tab A instantly affected Tab B. Users were confused. I was confused. My PM was not amused.
That's when I actually read the MDN Web Storage API docs and learned the real difference. It's one thing — persistence scope — but that one thing changes everything about when you should use each.
localStorage: Survives Everything Except a Clear
This persistence makes it perfect for things that should "just remember" across visits:
- **User preferences** — dark mode toggle, language selection, sidebar collapsed state - **Non-sensitive cached data** — API responses you want to avoid re-fetching on every visit - **Onboarding state** — has this user seen the welcome tour? - **Draft content** — auto-saved form text so users don't lose work if they accidentally close the tab
Here's a quick example I use on pretty much every project:
```javascript // Save theme preference localStorage.setItem('theme', 'dark');
// Retrieve it on page load const theme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', theme); ```
The data is scoped to the origin (protocol + domain + port). So `https://example.com` and `http://example.com` have separate localStorage. And it's shared across ALL tabs and windows for that origin. That sharing is the feature — and sometimes the problem, as I learned the hard way.
One more thing about localStorage that's worth noting: it's synchronous. Every `getItem()` and `setItem()` call blocks the main thread. For small amounts of data this is negligible — maybe a microsecond. But if you're reading and writing large objects frequently (like auto-saving a rich text editor every keystroke), the synchronous nature can cause jank. For heavy read/write workloads, consider IndexedDB, which is asynchronous. For typical preference and cache storage, localStorage is perfectly fine.
sessionStorage: Dies with the Tab
Photo by Mohammad Rahmani on Unsplash
Open `example.com` in Tab A and Tab B. Set a sessionStorage value in Tab A. Tab B can't see it. They're separate sessions.
This tab isolation is exactly what I needed for that product comparison feature. Each tab could maintain its own state without stepping on other tabs.
Good use cases for sessionStorage:
- **Multi-step form wizards** — store progress across pages within one tab session - **Tab-specific UI state** — scroll position, filter selections, active tab panels - **One-time authentication flows** — OAuth redirect data that shouldn't persist - **Shopping cart in a single session** — if you want carts to be tab-independent
```javascript // Store step progress in a multi-page form sessionStorage.setItem('checkout-step', '2'); sessionStorage.setItem('cart-items', JSON.stringify(items));
// On next page load within the same tab const step = sessionStorage.getItem('checkout-step'); ```
One gotcha: sessionStorage DOES survive page refreshes and navigating within the same tab. It's the tab closing that kills it. So if a user refreshes the page during a checkout flow, their progress isn't lost — but if they close the tab and open a new one, they start over. That's usually the behavior you want for transient workflows.
The Decision Framework I Actually Use
**1. Should this data outlive the current tab?** If yes → localStorage. If no → sessionStorage. Theme preferences, language settings, "don't show again" flags — those should stick around. Temporary wizard state, OAuth nonces, tab-specific search filters — those shouldn't.
**2. Should this data be shared across tabs?** If yes → localStorage. If no → sessionStorage. A user's notification preference should be consistent across all tabs. A draft email in progress should NOT appear in a second tab.
**3. Am I storing anything sensitive?** If yes → neither. Seriously. Neither localStorage nor sessionStorage is encrypted. Any JavaScript running on the page — including third-party scripts and browser extensions — can read the data. I've seen developers store JWT tokens in localStorage, and while it's a common pattern, it makes your app vulnerable to XSS attacks. If an attacker injects JavaScript into your page, they can steal every token in localStorage. HttpOnly cookies are safer for authentication tokens because JavaScript can't access them at all.
That third question is the most important one, and I've seen it ignored way too often. Trust me on this one.
Strings Only — and the JSON Workaround
The fix everyone uses:
```javascript // Storing an object const user = { name: 'Alex', preferences: { theme: 'dark' } }; localStorage.setItem('user', JSON.stringify(user));
// Retrieving it const stored = JSON.parse(localStorage.getItem('user')); console.log(stored.preferences.theme); // 'dark' ```
`JSON.stringify()` on the way in, `JSON.parse()` on the way out. Every time. If you work with JSON data regularly, tools like the ToolsFuel JSON formatter are handy for debugging what's actually stored — just paste the raw localStorage string and see the structure.
One edge case that bit me: `JSON.parse(null)` returns `null`, which is fine. But `JSON.parse(undefined)` throws a SyntaxError. Since `getItem()` returns `null` for missing keys (not `undefined`), you're usually safe. But if you're passing the key from a variable that might be undefined, wrap it in a try-catch or check first.
Another gotcha: Dates don't survive the JSON round-trip. `JSON.stringify` converts Date objects to ISO strings, and `JSON.parse` doesn't convert them back — you get a string, not a Date. If you're storing timestamps, store them as ISO strings or Unix timestamps and convert explicitly when reading. Always test your serialization and deserialization logic with edge cases before shipping.
Storage Events — the Feature You Didn't Know Exists
```javascript window.addEventListener('storage', (e) => { if (e.key === 'theme') { document.documentElement.setAttribute('data-theme', e.newValue); } }); ```
Now when a user toggles dark mode in one tab, every other tab updates automatically. No WebSockets, no polling, no shared state library. Just a browser-native event.
I used this on a dashboard project where users could have multiple panels open in different tabs. When they updated their notification settings in one tab, the other tabs reflected the change immediately. It felt magical and it was like four lines of code.
The event object gives you `key`, `oldValue`, `newValue`, and `url` (the page that triggered the change). One important note — the event only fires in OTHER tabs, not the tab that made the change. So you still need to update the current tab's UI directly after calling `setItem()`.
Size Limits and What Happens When You Hit Them
When you exceed the limit, `setItem()` throws a `QuotaExceededError`. If you're storing cached API responses or user-generated content, you should handle this:
```javascript try { localStorage.setItem('cache-data', JSON.stringify(largeObject)); } catch (e) { if (e.name === 'QuotaExceededError') { localStorage.removeItem('oldest-cache-key'); // retry or degrade gracefully } } ```
A few things I've learned about storage limits: don't store images as Base64 strings in localStorage. A single image can eat a megabyte. Don't cache entire API response lists when you only need a few fields. And if you're building something that genuinely needs more than 5-10 MB of client-side storage, look at IndexedDB instead — it's more complex but handles structured data at much larger scales.
I once debugged an issue where a user's app was crashing on load. Turned out a previous version had been aggressively caching data in localStorage without any cleanup, and after months of use it had hit the quota. The `setItem` call threw, the error wasn't caught, and the entire initialization script blew up. Always wrap storage writes in try-catch if there's any chance the data could grow over time.
Frequently Asked Questions
What is the main difference between localStorage and sessionStorage?
The main difference is persistence. localStorage keeps data until it's explicitly deleted — it survives tab closes, browser restarts, even computer reboots. sessionStorage only keeps data for the duration of the browser tab. Close the tab and the data is gone. Both have identical APIs and similar storage limits (around 5-10 MB), but this persistence difference determines which one you should use for each situation.
Is it safe to store JWT tokens in localStorage?
It's a common practice but not the safest option. localStorage is accessible to any JavaScript running on your page, including third-party scripts and potentially injected code from XSS attacks. If an attacker can run JavaScript on your page, they can steal tokens from localStorage. HttpOnly cookies are generally safer for authentication tokens because they're inaccessible to JavaScript. If you must use localStorage for tokens, make sure your app has strong XSS protections and uses short token expiration times.
Can different tabs access the same sessionStorage data?
No, each tab gets its own independent sessionStorage, even if they're on the same website. Setting a value in sessionStorage in Tab A won't be visible in Tab B. This tab isolation is by design and is actually useful for things like multi-step forms or tab-specific UI state where you don't want tabs interfering with each other. If you need data shared across tabs, use localStorage instead.
How much data can I store in localStorage and sessionStorage?
Most browsers allow around 5-10 MB per origin for each storage type. Chrome and Edge provide about 10 MB, Firefox gives 10 MB, and Safari provides around 5-10 MB depending on the version. When you exceed the limit, setItem() throws a QuotaExceededError. If you need more client-side storage, look into IndexedDB, which can handle much larger amounts of structured data.
Does sessionStorage survive a page refresh?
Yes, sessionStorage survives page refreshes and in-tab navigation. It only gets cleared when the browser tab is closed. So if a user is filling out a multi-step form and refreshes the page, their sessionStorage data is still there. This makes it good for storing temporary state within a single browsing session — the data persists through normal navigation but doesn't linger after the user closes the tab.
Why does localStorage only store strings?
The Web Storage API was designed to be simple and fast, and strings are the most universal data type across browsers. The standard workaround is to use JSON.stringify() when storing objects and JSON.parse() when retrieving them. Just watch out for edge cases: Date objects become strings after the JSON round-trip, and JSON.parse(undefined) throws an error while JSON.parse(null) returns null safely. Always validate what you're parsing.
Try ToolsFuel
23+ free online tools for developers, designers, and everyone. No signup required.
Browse All Tools