IndexedDB Overview
Browser IndexedDB for structured client storage — databases, object stores, transactions, indexes, and when not to use it.
- javascript
- indexeddb
- storage
- offline
IndexedDB is the browser’s transactional database for large structured data — offline caches, draft documents, queryable client stores. It’s async, origin-scoped, and far more capable (and awkward) than localStorage. Libraries (idb, Dexie) paper over the request API; interviews still want the mental model.
Core objects
| Piece | Role |
|---|---|
| Database | named, versioned container |
| Object store | like a table — holds records |
| Key | primary identity (in-line or out-of-line) |
| Index | secondary lookup path |
| Transaction | atomic read/write scope |
| Request | async operation with success/error |
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open('notes', 1);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains('notes')) {
const store = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true,
});
store.createIndex('by-updated', 'updatedAt');
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
Schema changes happen only in onupgradeneeded when the version bumps.
Transactions and CRUD
async function addNote(db, note) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
const req = store.add({ ...note, updatedAt: Date.now() });
req.onsuccess = () => resolve(req.result); // key
req.onerror = () => reject(req.error);
tx.onabort = () => reject(tx.error);
});
}
async function notesSince(db, ts) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly');
const index = tx.objectStore('notes').index('by-updated');
const range = IDBKeyRange.lowerBound(ts);
const req = index.getAll(range);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
Transactions auto-commit when the microtask checkpoint sees no more work — don’t await unrelated promises mid-transaction without keeping the tx alive carefully. Do all IDB work inside the tx callbacks or use a wrapper that understands this.
Keys and values
Values are structured-cloneable (objects, arrays, Blobs, …). Keys: numbers, strings, dates, arrays (with rules). No functions.
store.put(record); // upsert
store.get(id);
store.delete(id);
store.clear();
Versus localStorage / Cache API
| Need | Prefer |
|---|---|
| Tiny prefs | localStorage / cookies |
| HTTP response cache | Cache API (service worker) |
| Queryable large docs | IndexedDB |
| Sync API | not IDB |
Quota is generous but not infinite; listen for QuotaExceededError and storage eviction on mobile.
With idb (ergonomics)
import { openDB } from 'idb';
const db = await openDB('notes', 1, {
upgrade(db) {
db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
},
});
await db.add('notes', { title: 'Hi', updatedAt: Date.now() });
const all = await db.getAll('notes');
Use a library in production apps; explain raw IDB in interviews.
Footguns
- Version change while another tab has the DB open →
blocked/versionchangeevents; close connections. - Expecting SQL — no joins; denormalize or filter in JS.
- SSR —
indexedDBis browser-only. - Private mode / disabled storage — open can fail; always handle errors.
Interview answer
“IndexedDB is an async, transactional, origin-scoped store for structured data. You open a versioned DB, create object stores and indexes in onupgradeneeded, and run reads/writes inside transactions. Values use structured clone. I use it for offline/large client data; localStorage for tiny sync prefs. Wrappers like idb improve ergonomics.”
Related
Further reading
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.