v145 · Web APIs · API Patterns

API Patterns

Common IndexedDB upsert patterns — idempotent writes, multi-tab safe updates, partial field merges — and when to use put() versus add().

put() is always upsert in IndexedDB: it inserts if absent, replaces if present. add() is insert-only and throws a ConstraintError if the key already exists. Use add() when duplicate prevention is the goal; use put() for everything else.

idempotent setting store

// Settings store — safe to call multiple times with same key
async function saveSetting(db, key, value) {
  const tx = db.transaction('settings', 'readwrite');
  tx.objectStore('settings').put({ key, value, savedAt: Date.now() });
  return tx.complete ?? new Promise((r, e) => {
    tx.oncomplete = r;
    tx.onerror = () => e(tx.error);
  });
}

// Called on every render — idempotent, no duplicate check needed
await saveSetting(db, 'volume', 0.8);
await saveSetting(db, 'volume', 0.8); // Same result, no error

multi-tab safe last-write-wins

// When multiple tabs write the same key, put() guarantees
// the last write wins — no read needed, no data corruption.

// Tab A and Tab B both call this concurrently:
async function updatePresence(db, userId, status) {
  const tx = db.transaction('presence', 'readwrite');
  tx.objectStore('presence').put({
    id: userId,
    status,
    ts: Date.now(),
  });
}

// One write will succeed; the other will overwrite (last-write-wins).
// This is safe — no partial write, no silent failure.

partial field merge (read-modify-write)

// When you only want to update specific fields and preserve others:
async function mergeRecord(db, store, id, patch) {
  const tx = db.transaction(store, 'readwrite');
  const objStore = tx.objectStore(store);

  const existing = await new Promise(r => {
    const req = objStore.get(id);
    req.onsuccess = () => r(req.result);
  });

  // Merge patch into existing, then upsert
  objStore.put({ ...existing, ...patch, id, updatedAt: Date.now() });
}

// Usage:
await mergeRecord(db, 'users', 'usr_123', { role: 'admin' });
// Preserves name, email, etc.; only updates role

put() vs add() decision

// put() — upsert (insert or replace)
store.put({ id: key, value }); // always succeeds

// add() — insert only (throws ConstraintError if key exists)
store.add({ id: key, value }); // fails if key already exists

// Use add() when:
// - You want to prevent accidental overwrites
// - You're creating a new record and conflict = error
// - e.g., creating a new user account (email must be unique)

// Use put() when:
// - You want idempotent writes
// - You don't care if the record exists or not
// - You're syncing or caching remote data

see also

scenario focus

Select a scenario to focus its rendered example and summary.