When to use IndexedDB

IndexedDB is a browser API for storing structured data on a user’s device. It is useful when an interface needs to keep more than a small preference: saved drafts, an offline list, or data that would be awkward to fit into strings. Unlike localStorage, IndexedDB operations are asynchronous, so a read or write does not need to block the page while it finishes.

IndexedDB data belongs to an origin, such as the combination of scheme, host, and port serving your application. It is browser-side storage, not a way to share records between devices or users. Browsers can impose storage limits, and users can clear site data, so keep an important copy elsewhere when losing the local data would be unacceptable.

Understand databases, stores, and keys

An IndexedDB database contains one or more object stores. An object store holds records, each identified by a key. You can supply the key separately or designate a property of each stored object as its key. In the example below, a store named notes uses each note’s id property. Saving another note with the same id replaces the existing record because the example uses put().

Database structure is set during a version upgrade. Opening a database with a higher version triggers an upgradeneeded event, where you can create a store or change its indexes. Ordinary reads and writes happen later, inside transactions. A transaction is scoped to particular stores and has a mode: readonly for retrieval or readwrite for changes.

Create a store and save a note

To try this example, open a page served from a local development server, such as http://localhost, or an HTTPS site, then paste the following JavaScript into that page’s browser developer console. Use a page whose stored data you are comfortable changing. The code opens a database, creates the notes store if it does not exist, and saves one object.

const openRequest = indexedDB.open('notes-demo', 1); openRequest.onupgradeneeded = () => { const db = openRequest.result; if (!db.objectStoreNames.contains('notes')) { db.createObjectStore('notes', { keyPath: 'id' }); } }; openRequest.onerror = () => console.error('Open failed:', openRequest.error); openRequest.onsuccess = () => { const db = openRequest.result; const writeTx = db.transaction('notes', 'readwrite'); writeTx.objectStore('notes').put({ id: 'first-note', title: 'Try IndexedDB', done: false }); writeTx.onerror = () => console.error('Write failed:', writeTx.error); writeTx.oncomplete = () => { console.log('Note saved'); db.close(); }; };

The object contains strings and a boolean, but IndexedDB can store many other values supported by the structured clone algorithm. It does not store a live reference to your original object. Notice also that the message confirming the save is attached to the transaction’s complete event. Calling put() starts a request; it does not, by itself, confirm that the transaction committed.

Retrieve the saved object

After the console prints “Note saved,” run this separate snippet on the same page and origin. It opens the existing database, starts a readonly transaction, and looks up the key used above.

const openRequest = indexedDB.open('notes-demo', 1); openRequest.onerror = () => console.error('Open failed:', openRequest.error); openRequest.onsuccess = () => { const db = openRequest.result; const readTx = db.transaction('notes', 'readonly'); const getRequest = readTx.objectStore('notes').get('first-note'); getRequest.onsuccess = () => console.log('Found:', getRequest.result); readTx.onerror = () => console.error('Read failed:', readTx.error); readTx.oncomplete = () => db.close(); };

You should see the note object in the console. If no record has that key, getRequest.result is undefined. The request’s success event means the lookup finished, not that a matching record necessarily exists. For an interface, handle the missing-record case explicitly—for example, by showing an empty state rather than trying to display an undefined title.

Make storage code dependable

Keep transaction work together. In particular, avoid starting a transaction and then waiting for unrelated asynchronous work before adding its requests; the browser may have finished the transaction by then. Prepare data first, start the transaction, and queue its IndexedDB requests promptly. Check request or transaction errors, and use transaction completion when the next action depends on a committed write.

As an application grows, put database-opening and transaction logic in reusable functions rather than repeating event handlers throughout components. Plan schema changes by increasing the database version and updating stores inside the upgrade handler. An upgrade may be blocked while another tab keeps an older connection open, so close connections when they are no longer needed. Finally, test the missing-data and cleared-storage paths: locally stored data is convenient, but a resilient interface must still work when it is absent.

Back to category ↗