Lesson 0003 · ~14 minutes
One skill: run reads and writes inside a correctly scoped transaction — and avoid the lifetime traps that bite production FE code.
You can sketch transaction → store → get/put/add/delete,
pick readonly vs readwrite, explain auto-commit, and flag “await
outside the request chain” as a design-review red flag.
Per MDN’s Using IndexedDB and the spec’s transaction model: you never “just write a row.” You open a transaction that scopes which object stores you touch and whether you may write.
const tx = db.transaction("notes", "readonly");
const store = tx.objectStore("notes");
const req = store.get(id);
req.onsuccess = () => {
console.log(req.result); // value or undefined
};
tx.oncomplete = () => { /* committed */ };
tx.onerror = () => { /* aborted or request failed */ };
Multi-store work: pass an array —
db.transaction(["notes", "tags"], "readwrite"). Scope is a
design choice: too wide and you block more writers; too narrow and you
cannot keep multi-store invariants atomic.
Default for loads. Multiple readonly transactions can overlap. Prefer this unless you must mutate.
Mutations: put, add, delete,
clear. Writers on overlapping stores serialize — plan
short transactions.
Only from upgradeneeded (lesson 2). Schema work, not
app CRUD.
| Call | Meaning | Design note |
|---|---|---|
get(key) |
One record by primary key |
result is the value, or undefined if
missing
|
getAll(query?, count?) |
Many records (optional key range) | Fine for modest sets; huge dumps need cursors / paging |
put(value, key?) |
Insert or overwrite | Idempotent upsert for the same key |
add(value, key?) |
Insert only | Fails if the key already exists — use for “create once” |
delete(key) |
Remove one key | Ok if missing (no error for absent key) |
clear() |
Empty the store | Dangerous product action — treat like truncate |
Out-of-line keys: pass the key as the second argument to
put/add. In-line keys: the key lives on the
value via keyPath (lesson 2).
// In-line keyPath: "id"
store.put({ id: "n1", body: "hello" });
// Out-of-line store: key is separate
store.put({ body: "hello" }, "n1");
// Create-only
store.add({ id: "n1", body: "first" }); // error if n1 exists
Transactions are tied to the event loop. MDN’s rule of thumb:
success/error handlers.
Do not await network, timers, or other
unrelated async work in the middle of an IndexedDB transaction and
expect to keep issuing store requests. That is how you get
InvalidStateError: The transaction has finished (or
“transaction inactive”). Finish IDB work in the request chain (or use
a promise wrapper that keeps the chain correct), then do other async
work outside.
// BAD: await leaves the turn; tx may already be done
const tx = db.transaction("notes", "readwrite");
const store = tx.objectStore("notes");
const note = await fetchNoteFromServer(); // leaves event loop
store.put(note); // often fails — transaction inactive/finished
// GOOD: IDB work contiguous; other async outside
const note = await fetchNoteFromServer();
const tx = db.transaction("notes", "readwrite");
tx.objectStore("notes").put(note);
// wait for tx.oncomplete (or promise wrap) if you need “saved”
Libraries like
idb
make the API promise-shaped, but they do not repeal the lifetime model
— they help you stay inside it.
A single put’s onsuccess means the
request finished; the transaction may still hold more work.
Durability for “user can close the tab” is
transaction.oncomplete (or the equivalent promise from a
wrapper). On constraint errors (duplicate key on add,
bad key), the request errors and typically aborts the transaction —
plan user-visible failure, not silent partial saves.
Deep indexes, cursors, and multi-tab live sync are next layers. Today is the mental model that stops most “IDB is flaky” bugs: scope, mode, verbs, lifetime.
equal-length choices. Immediate feedback.
How do you legally write one note record after the DB is already open?
Product rule: “never overwrite an existing draft id.” Which store call matches that?
Which pattern is a design-review red flag for IndexedDB?
When is it safest to tell the user “saved locally” after a multi-step write?
Loading a settings object by id with no mutation. Best mode?
put upserts; add creates once; prefer
readonly when you only read.
await.
MDN — Using IndexedDB
— focus on creating/starting transactions, reading/writing data, and
the sections on transaction lifetime. Optional second pass:
web.dev — Work with IndexedDB
(promise-oriented via idb).