IndexedDB Guide: JavaScript Database Transactions

IndexedDB is a powerful, low-level client-side storage API built into modern web browsers, designed for handling large amounts of structured data, including files and blobs. This article explains the core concepts of IndexedDB and details step-by-step how JavaScript uses transactional database operations to guarantee reliable, atomic, and safe data management within the browser.

What is IndexedDB?

IndexedDB is an asynchronous, transactional, object-oriented database embedded directly within the browser. Unlike localStorage, which is synchronous, limited to 5MB, and restricted to simple strings, IndexedDB offers significant advantages:

Understanding Transactions in IndexedDB

Every read or write operation in IndexedDB must occur within the scope of a transaction. Transactions ensure data integrity through the ACID principles (Atomicity, Consistency, Isolation, Durability).

If an operation inside a transaction fails, all changes made within that transaction are rolled back automatically, preventing partial or corrupted data states.

Transaction Modes

JavaScript supports three primary transaction modes:

  1. readonly: Permits read operations only. Multiple readonly transactions can access the same object store simultaneously.
  2. readwrite: Permits reading, adding, updating, and deleting data. Only one readwrite transaction can modify an object store at a given time.
  3. versionchange: A special transaction used exclusively during database creation or structural schema upgrades (creating/deleting object stores or indexes).

How JavaScript Executes Transactional Operations

Performing database operations in JavaScript follows a distinct lifecycle: opening a connection, defining the schema, creating a transaction, and executing database requests.

1. Opening the Database and Schema Setup

A database connection is initiated with indexedDB.open(). Schema changes, such as creating object stores, occur inside the onupgradeneeded event handler.

const request = indexedDB.open("AppDatabase", 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  if (!db.objectStoreNames.contains("users")) {
    db.createObjectStore("users", { keyPath: "id" });
  }
};

2. Creating a Transaction and Accessing the Object Store

Once the database is open (onsuccess), you create a transaction by specifying target object stores and the access mode, then retrieve the object store reference.

request.onsuccess = (event) => {
  const db = event.target.result;

  // 1. Create a transaction
  const transaction = db.transaction(["users"], "readwrite");

  // 2. Access the object store
  const store = transaction.objectStore("users");

  // 3. Perform the operation
  const addRequest = store.add({ id: 101, name: "Jane Doe", email: "jane@example.com" });

  addRequest.onsuccess = () => {
    console.log("Record added successfully.");
  };

  // 4. Handle transaction lifecycle events
  transaction.oncomplete = () => {
    console.log("Transaction completed successfully.");
  };

  transaction.onerror = (err) => {
    console.error("Transaction failed:", err.target.error);
  };
};

3. Reading and Modifying Data

Common data operations use specific methods available on the object store within a transaction:

4. Transaction Lifecycle and Auto-Commit

In JavaScript, IndexedDB transactions commit automatically when all queued requests complete and the active event loop tick finishes without new requests being added. If an explicit cancellation is needed, invoking transaction.abort() instantly rolls back all pending and completed changes within that transaction scope.