Debugging browser storage: cookies, localStorage, and IndexedDB
A tester reports that the currency selector on https://staging.example.com always snaps back to euros. Two colleagues try it and see dollars, as expected. The build is identical. Nothing in the network tab looks wrong.
The cause is one line of old data: a key called user_prefs in localStorage, written by a version of the app that shipped three months ago, still holding {"currency":"EUR"}. The new code reads the key and never validates it.
Browser storage is where "works on my machine" bugs live.
Short version
- Browsers keep data in three main places: cookies, localStorage or sessionStorage, and IndexedDB.
- The Application panel in Chrome DevTools shows and edits all three.
- Most storage bugs are stale state: new code reading data written by old code.
- To prove one, show it broken with the data present and working after you delete that one key.
- A hard reload does not clear storage. Only Clear site data or manual deletion does.
The three kinds of storage, in plain terms
Cookies are small pieces of text the browser sends back to the server with every matching request. They are how a server knows who you are. A session cookie is the classic example.
localStorage and sessionStorage are simple key-and-value stores that live only in the browser. They are never sent to the server automatically. localStorage survives a browser restart; sessionStorage disappears when the tab closes.
IndexedDB is a database inside the browser. It holds structured objects, not just text, and is used for offline data, caches, and large lists.
| Cookies | localStorage | sessionStorage | IndexedDB | |
|---|---|---|---|---|
| Sent to server | Yes, automatically | No | No | No |
| Survives restart | Until it expires | Yes | No | Yes |
| Typical size limit | About 4 KB each | About 5 MB per site | About 5 MB per site | Hundreds of MB |
| Holds | Text | Text | Text | Objects and files |
| Common use | Login sessions | Preferences, flags | One-visit state | Offline data, caches |
One rule follows from the table. If data is wrong and the server never saw it, cookies are not your problem. Look at localStorage or IndexedDB.
The Application panel, step by step
Everything below is in Chrome or Edge. Firefox has the same tools under a tab called Storage.
- Open DevTools with
F12, orCmd+Option+Ion a Mac. - Click the Application tab. If you do not see it, click the
>>arrow at the end of the tab bar. - In the left sidebar, find the Storage section. It lists Local Storage, Session Storage, IndexedDB, and Cookies.
- Click Local Storage, then click your site's origin, for example
https://staging.example.com. You now see a table of keys and values. - Click any row. The full value appears in the preview pane at the bottom, which matters because long JSON is cut off in the table.
- Double-click a value to edit it in place. Press
Enterto save, then reload the page to see the effect. - Right-click a row and choose Delete to remove one key, which is the single most useful action in this panel.
For cookies, click Cookies and then the origin. You get extra columns that often hold the answer: Expires, HttpOnly, Secure, SameSite, and Path.
For IndexedDB, expand the database, then the object store. Click Refresh in the panel toolbar, because IndexedDB views do not update on their own and will happily show you data from five minutes ago.
Reading and editing from the Console
The panel is fine for browsing. The Console is faster for capturing evidence, because you can copy the output straight into a ticket.
// Every localStorage key and value, as readable JSON
JSON.stringify(localStorage, null, 2)
// One key, parsed
JSON.parse(localStorage.getItem('user_prefs'))
// All cookies visible to JavaScript
document.cookie
// Remove one key, then reload
localStorage.removeItem('user_prefs'); location.reload()
Note what document.cookie does not show. Cookies marked HttpOnly are hidden from JavaScript on purpose, so they only appear in the Application panel. If a login cookie seems missing from the Console, check the panel before reporting it.
Why stale state creates bugs
Storage bugs almost always follow the same pattern. Code changes. Data does not.
Your app writes a key today, ships a new version next month that expects a different shape, and reads the old value without checking it. Users who have visited before hit the bug. New users never do, which is why the developer with a fresh profile cannot reproduce it.
Four versions of this pattern cover most cases:
Shape changed. Version 1 wrote "EUR". Version 2 expects {"code":"EUR"}. The new code calls prefs.currency.code and the console shows TypeError: Cannot read properties of undefined (reading 'code').
Value no longer valid. The key holds a saved filter for a project that has since been deleted. Every page load requests a missing resource and shows an empty list.
Two sources of truth. The server says the plan is Pro, localStorage still says Free, and the UI trusts the cached copy. The user sees features disappear after a refresh.
Storage is full. localStorage has a limit of roughly 5 MB. When it is full, writes throw QuotaExceededError. If the app does not handle it, saving silently stops working while everything else looks fine.
A quick test tells you which family you are in. Open a private window, do the same steps, and see whether the bug survives. Private windows start with empty storage.
Proving a storage bug in three steps
"I think it is a storage thing" gets a ticket closed. A proof does not. Do this instead.
- Capture the bad state. With the bug on screen, run
JSON.stringify(localStorage, null, 2)in the Console and copy the output. Screenshot the Application panel row too, so the key name and value are visible together. - Change one thing. Delete only the suspected key, then reload. Do not clear everything, because clearing everything proves nothing about which key mattered.
- Show it works, then put it back. If the bug is gone, you have your answer. Paste the old value back with
localStorage.setItem('user_prefs', '...')and reload to confirm the bug returns. A bug that comes back on demand is not a coincidence.
That third step is what turns a guess into evidence. Most testers skip it.
What to write in the report
A developer needs the key, the value, the code that read it, and the effect. Give them the first two and they will find the rest.
Bad
Currency keeps resetting to euros. Clearing the cache fixes it. Maybe a caching issue.
Good
Currency resets to EUR on every load for accounts that used the site before 12 May.
Storage:
localStorage.user_prefs = {"currency":"EUR","v":1}Console:TypeError: Cannot read properties of undefined (reading 'code')atprefs.ts:44Deleting onlyuser_prefsand reloading fixes it. Re-adding the same value brings the bug back, 3 of 3 times. Environment: Chrome 141, macOS 15,[email protected],https://staging.example.com
Copying the storage dump, console error, and environment lines by hand is the slow part, and it is where details get dropped. A browser-based reporting tool such as Crosscheck captures the console output, network requests, and environment details from the page while you write the report, which leaves you only the storage key to paste in.
Clearing storage properly
"Clear the cache" is the most misleading instruction in bug reports, because a normal cache clear leaves storage untouched.
What each action actually does:
- Normal reload (
F5). Nothing is cleared. - Hard reload (
Ctrl+Shift+R). Refetches files. Cookies, localStorage, and IndexedDB survive. - Empty cache and hard reload. Right-click the reload button with DevTools open. Clears the HTTP cache only. Storage survives.
- Application panel, Storage, Clear site data. Clears everything for that origin, including cookies, localStorage, IndexedDB, and service workers.
- New private window. No stored data at all, and nothing is destroyed for your normal profile.
For QA work, prefer the private window for a clean run and targeted deletion when investigating. Save Clear site data for when you truly want a blank slate, and remember it logs you out.
One more trap. If your site uses a service worker, which is a script that can serve cached responses without touching the network, you may keep seeing old files after clearing storage. Unregister it under Application, then Service Workers, and reload.
Frequently asked questions
Why can I not see a cookie in the Console that appears in the Application panel?
It is almost certainly marked HttpOnly, which hides it from JavaScript to protect session tokens. That is intended behaviour, not a bug.
How much can localStorage hold before it breaks?
About 5 MB per origin in most browsers, though the exact figure varies. When it is exceeded, writes throw QuotaExceededError, so watch for saves that quietly stop working while the rest of the page behaves.
Should test data go in localStorage or sessionStorage?
Use sessionStorage for anything that should not survive the tab, such as a wizard step. Reserve localStorage for real preferences. Many stale-state bugs exist only because sessionStorage would have been the right choice.
Does clearing storage affect other testers on the same environment?
No. Storage lives in your browser profile, so clearing it only affects you. Changing server-side data or a shared test account does affect others.
How do I check storage on a mobile browser?
Connect the device and use remote debugging, which gives you the same Application panel from your desktop Chrome or Safari. Without a cable, ask the app to show the relevant values on an internal debug screen.




