A map of the IndexedDB object graph
IndexedDB is a transactional, asynchronous, key-value store built into the browser for large structured data and offline apps. Its API spans several interfaces — a factory, the database, object stores, indexes, cursors and transactions — each with its own methods. This searchable reference lists the core members with their signatures so you can find the right call quickly.
How it works
You open a database, declare stores and indexes during an upgrade, then read and write inside transactions:
const req = indexedDB.open("app", 2);
req.onupgradeneeded = (e) => {
const db = e.target.result;
const store = db.createObjectStore("notes", { keyPath: "id" });
store.createIndex("by_tag", "tag", { unique: false });
};
req.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction("notes", "readwrite");
tx.objectStore("notes").put({ id: 1, tag: "todo", body: "ship it" });
};
Every operation returns an IDBRequest whose onsuccess/onerror fire
asynchronously. Transactions auto-commit when control returns to the event loop
with no pending requests. Schema changes are confined to the special
versionchange transaction during upgradeneeded.
Tips and notes
- Use
keyPathfor in-line keys (the key lives in the value) orautoIncrementfor generated keys. - Cursors iterate in key order; pass
prevfor descending ornextuniqueto skip duplicate index keys. getAll()is faster than a cursor when you simply need every matching value as an array.- A thrown error or
tx.abort()rolls back every change made in that transaction — IndexedDB is fully atomic per transaction.
Common patterns and pitfalls
Reading inside the same transaction you wrote to
Transactions auto-commit once the JavaScript event loop returns with no pending requests. This means you cannot open a readwrite transaction, do a put, return to the event loop, and then re-use the same transaction handle for a later read. The transaction will already be committed (or aborted). Chain all related reads and writes within a single transaction’s request callbacks.
Multiple object stores in one transaction
A single transaction can span multiple object stores — pass an array of names to db.transaction(['store1', 'store2'], 'readwrite'). This is important when you need to keep two stores consistent, since only a single transaction provides the atomicity guarantee.
Index range queries
Use IDBKeyRange with openCursor() or getAll() on an index to retrieve a bounded set of records without loading the whole store:
const idx = tx.objectStore('notes').index('by_tag');
const range = IDBKeyRange.only('todo');
idx.getAll(range).onsuccess = (e) => console.log(e.target.result);
This is substantially faster than cursoring the whole store and filtering in JavaScript.
Version migrations
Migrations run inside onupgradeneeded. The event fires every time the version number increases, and event.oldVersion tells you which version the database was on before. Use a sequential upgrade pattern:
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (e.oldVersion < 1) {
db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
}
if (e.oldVersion < 2) {
db.transaction.objectStore('notes').createIndex('by_tag', 'tag');
}
};
This lets users who missed a version catch up through all migrations in order.
Error handling
Every IDBRequest has an onerror handler, and every transaction has onerror and onabort. If you do not handle errors, they bubble to the database and then to window.onerror. A common defensive pattern is to attach an onerror to the transaction itself to catch any uncaught request error and log or report it.
Storage limits
IndexedDB is subject to the browser’s storage quota, which varies by origin and can be as low as a few hundred megabytes on mobile devices. For very large datasets, check navigator.storage.estimate() before writing, and handle the QuotaExceededError DOMException gracefully.