ESC

Type to search the knowledge base.

IndexedDB Overview

Browser IndexedDB for structured client storage — databases, object stores, transactions, indexes, and when not to use it.

advanced3 min read
  • 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

  1. Version change while another tab has the DB open → blocked / versionchange events; close connections.
  2. Expecting SQL — no joins; denormalize or filter in JS.
  3. SSR — indexedDB is browser-only.
  4. 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.”

Further reading

Related guides