Lesson 0003 · ~14 minutes

Transactions & CRUD

One skill: run reads and writes inside a correctly scoped transaction — and avoid the lifetime traps that bite production FE code.

Win for this lesson

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.

1. Everything is a transaction

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.

2. Modes you actually use

readonly

Default for loads. Multiple readonly transactions can overlap. Prefer this unless you must mutate.

readwrite

Mutations: put, add, delete, clear. Writers on overlapping stores serialize — plan short transactions.

versionchange

Only from upgradeneeded (lesson 2). Schema work, not app CRUD.

3. The CRUD verbs

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

4. Lifetime: the production footgun

Transactions are tied to the event loop. MDN’s rule of thumb:

Default rule (steal this)

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.

5. complete vs request success

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.

What this lesson is not

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.

6. Practice

equal-length choices. Immediate feedback.

Scenario A

How do you legally write one note record after the DB is already open?

Scenario B

Product rule: “never overwrite an existing draft id.” Which store call matches that?

Scenario C

Which pattern is a design-review red flag for IndexedDB?

Scenario D

When is it safest to tell the user “saved locally” after a multi-step write?

Scenario E

Loading a settings object by id with no mutation. Best mode?

7. What to remember

Ask your teacher Fuzzy on inactive transactions, put vs add, or how this maps to a feature you’re building? Ask in chat.

Primary source (read next)

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).