Transaction
Two forms. Both are atomic; they differ in whether the callback can read.
Interactive — Dexie-compatible
await db.transaction('rw', db.friends, db.notes, async () => {
const n = await db.friends.count(); // a READ inside the transaction
await db.notes.add({ owner: `friend-${n}` });
});Same signature as Dexie's transaction(): mode 'r' or 'rw', the tables, then an async callback. Throwing rolls the whole thing back.
Isolation. The transaction holds an exclusive cross-tab Web Lock plus a real SQLite transaction for its duration. Ordinary calls take a shared lock, which is what stops another tab's writes landing inside your transaction. Nesting is allowed and joins the outer transaction (SQLite has a single write transaction).
Batch — the fast path
When you don't need to read, this is one round trip instead of several:
await db.transaction((tx) => {
tx.friends.add({ name: 'eve', age: 22 });
tx.notes.add({ owner: 'eve' });
});The callback is synchronous and records operations, which are shipped as a single atomic batch. Passing an async callback throws a clear error telling you to use the interactive form.
Returns an array of each operation's result.
Which to use
| Interactive | Batch | |
|---|---|---|
| Can read | ✅ | ❌ |
| Round trips | one per statement | one total |
| Holds a cross-tab lock | ✅ exclusive | no |
| Dexie-compatible signature | ✅ | — |
Reach for the batch form for known write sets (imports, bulk edits, "save this form"), and the interactive form when a later write depends on an earlier read.
Durability caveat
If the leader tab dies mid-transaction, SQLite rolls back automatically — the connection died with it.
What the caller sees depends on how far its call got. If the leader died after acknowledging it, the commit state is unknown and you get a LeaderLostError — verify before retrying. If it was never acknowledged, nothing ran and retrying is safe, including when the leader was merely frozen rather than dead. See Errors.