CRUD Operations
The CRUD method reference
Section titled “The CRUD method reference”Every data operation in IndexedDB goes through an IDBObjectStore. You obtain a store from a transaction, then call one of these six methods:
| Method | Mode needed | Description |
|---|---|---|
add(value) | readwrite | Adds a new record; throws ConstraintError if the key already exists |
put(value) | readwrite | Adds or overwrites (upsert); never throws on duplicate key |
get(key) | readonly | Returns one record, or undefined if the key is absent |
getAll() | readonly | Returns an array of all records in the store |
delete(key) | readwrite | Removes the record with the given key; no-op if absent |
count() | readonly | Returns the total number of records |
Every method returns an IDBRequest. The result is available inside the request’s onsuccess callback as request.result.
add vs put
Section titled “add vs put”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 yetstore.add({ id: 1, name: 'Apple', price: 1.5 });
// put — always safe; overwrites if id 1 is already therestore.put({ id: 1, name: 'Apple', price: 1.99 });get returns undefined, not null
Section titled “get returns undefined, not null”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};Wrapping IDBRequest in a Promise
Section titled “Wrapping IDBRequest in a Promise”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 methodconst apple = await promisifyReq(store.get(1));Runnable: complete CRUD demo
Section titled “Runnable: complete CRUD demo”Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
add() vs put() | add prevents a duplicate key with a ConstraintError, guaranteeing a genuinely new record | Requires extra error handling for the duplicate-key case, unlike put, which upserts for you |
getAll() vs iterating a cursor | One call returns the whole array — short, readable code | Loads every record into memory at once, unsuitable for a large store |
Wrapping IDBRequest in a Promise | Readable async/await code, less callback nesting | Adds an abstraction layer; you must still handle onerror for every request or errors disappear silently |
Common mistakes
Section titled “Common mistakes”- Checking
if (!result)instead ofresult === undefined— a record whose value is falsy, like0or'', gets treated as missing even though it exists. Always check=== undefined. - Using
add()without handlingonerror— on a duplicate key the request throwsConstraintError, but with noonerrorhandler 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 throwsTransactionInactiveErrorimmediately.