Skip to content

CRUD Operations

Every data operation in IndexedDB goes through an IDBObjectStore. You obtain a store from a transaction, then call one of these six methods:

MethodMode neededDescription
add(value)readwriteAdds a new record; throws ConstraintError if the key already exists
put(value)readwriteAdds or overwrites (upsert); never throws on duplicate key
get(key)readonlyReturns one record, or undefined if the key is absent
getAll()readonlyReturns an array of all records in the store
delete(key)readwriteRemoves the record with the given key; no-op if absent
count()readonlyReturns the total number of records

Every method returns an IDBRequest. The result is available inside the request’s onsuccess callback as request.result.

add(value) is strictly for new records. If a record with the same key path value already exists, the request fires onerror with a ConstraintError — the record is not written.

put(value) is an upsert: it writes the record whether or not the key already exists. Use put when you want “create or update” semantics without checking first.

const tx = db.transaction(['products'], 'readwrite');
const store = tx.objectStore('products');
// add — safe only when id 1 does not exist yet
store.add({ id: 1, name: 'Apple', price: 1.5 });
// put — always safe; overwrites if id 1 is already there
store.put({ id: 1, name: 'Apple', price: 1.99 });

This is different from Web Storage, where a missing key returns null. With IndexedDB, get(key) resolves to undefined when no record matches. Always check === undefined, not !result, to distinguish a missing record from a record that stores a falsy value.

const tx = db.transaction(['products'], 'readonly');
const store = tx.objectStore('products');
const req = store.get(999);
req.onsuccess = () => {
console.log(req.result); // undefined — key 999 does not exist
};

Raw IDBRequest callbacks are verbose. A tiny helper makes every store method await-able:

function promisifyReq(r) {
return new Promise((res, rej) => {
r.onsuccess = () => res(r.result);
r.onerror = () => rej(r.error);
});
}
// Now you can await any store method
const apple = await promisifyReq(store.get(1));
Browser Storage
OptionBenefitCost
add() vs put()add prevents a duplicate key with a ConstraintError, guaranteeing a genuinely new recordRequires extra error handling for the duplicate-key case, unlike put, which upserts for you
getAll() vs iterating a cursorOne call returns the whole array — short, readable codeLoads every record into memory at once, unsuitable for a large store
Wrapping IDBRequest in a PromiseReadable async/await code, less callback nestingAdds an abstraction layer; you must still handle onerror for every request or errors disappear silently
  • Checking if (!result) instead of result === undefined — a record whose value is falsy, like 0 or '', gets treated as missing even though it exists. Always check === undefined.
  • Using add() without handling onerror — on a duplicate key the request throws ConstraintError, but with no onerror handler the error disappears silently while the record is never written.
  • Awaiting fetch() or a similar call in the middle of a chain of store method calls — the transaction that store method belongs to auto-commits as soon as the call stack empties, so the very next request throws TransactionInactiveError immediately.
You call store.add({ id: 1, name: "Apple" }) when a record with id 1 already exists. What happens?
What does store.get(999) resolve to when no record has key 999?
Which transaction mode is required to call store.delete(key)?
You want to update a record if it exists, or insert it if it does not — in one call. Which method do you use?