Transactions
What is a Transaction?
Section titled “What is a Transaction?”A transaction is a group of database operations that execute as a single atomic unit. This follows the ACID principle: either all operations in the transaction succeed, or none of them are applied. If any operation fails, the entire transaction is rolled back to the state before it started.
In IndexedDB, every read and write must happen inside a transaction. There is no way to access an object store directly — you always go through a transaction first.
Opening a Transaction
Section titled “Opening a Transaction”Use db.transaction() to open a new transaction. It takes two arguments: an array of store names you want to access, and a mode string.
// Read-only transaction — for reads onlyconst tx = db.transaction(['storeName'], 'readonly');
// Read-write transaction — for reads and writesconst tx = db.transaction(['storeName'], 'readwrite');Once you have a transaction, call tx.objectStore('storeName') to get the IDBObjectStore reference you need to issue requests:
const store = tx.objectStore('storeName');const request = store.get(42);Transaction Modes
Section titled “Transaction Modes”IndexedDB supports two modes:
readonly— Multiple readonly transactions on the same stores can run concurrently. Use this whenever you only need to read data.readwrite— Acquires an exclusive lock on each store listed. Only one readwrite transaction can access a given store at a time. Other transactions wait until it completes.
Always prefer readonly when you do not need to write — it allows better parallelism and avoids unnecessary locking.
Listening to Transaction Events
Section titled “Listening to Transaction Events”A transaction fires three events that you should handle:
tx.oncomplete = () => { // All requests inside the transaction succeeded and were committed console.log('Transaction committed successfully');};
tx.onerror = (event) => { // At least one request failed — the transaction was rolled back console.error('Transaction error:', tx.error);};
tx.onabort = () => { // The transaction was explicitly aborted via tx.abort() console.warn('Transaction aborted');};oncomplete fires only after every request inside the transaction has finished successfully and the changes have been flushed to disk. This is the right place to confirm to the user that data was saved.
Auto-Commit Behaviour
Section titled “Auto-Commit Behaviour”IndexedDB transactions use auto-commit: when the last pending request in the transaction finishes and the JavaScript call stack returns to the event loop with no new requests queued, the transaction automatically commits.
This means you must issue all requests in a synchronous chain — one request’s success handler triggers the next request. If you yield to the event loop between requests (for example by awaiting a fetch() call or wrapping logic in setTimeout), the transaction will already have committed by the time your code resumes, and any new requests against it will throw an error.
// WRONG — do not do thistx.oncomplete = async () => { /* ... */ }; // not the right pattern
const store = tx.objectStore('items');store.get(1).onsuccess = async (e) => { const data = await fetch('/api/extra'); // yields to event loop — transaction is gone! store.put({ ...e.target.result, extra: data }); // ERROR: transaction has already committed};// CORRECT — keep all requests in the same synchronous microtask chainconst store = tx.objectStore('items');const getReq = store.get(1);getReq.onsuccess = (e) => { store.put({ ...e.target.result, updated: true }); // issued synchronously — still in tx};Runnable Example
Section titled “Runnable Example”The code below opens a database, creates an items object store, opens a readwrite transaction, adds a record, and waits for oncomplete before closing.