IndexedDB Versioning and Upgrade Mechanism Explained
IndexedDB uses a strict, version-based schema management system in
JavaScript to handle changes to database structures over time. Whenever
you need to create object stores, modify indexes, or restructure
existing data, you must increment the database version number. This
triggers a dedicated upgradeneeded event, which runs a
special schema-modifying transaction that ensures database changes are
applied safely, atomically, and without corrupting existing records.
Database Versioning Basics
In IndexedDB, database versions are represented as positive integers
(e.g., 1, 2, 3). Floating-point
numbers are converted to integers, and setting a version lower than the
current version will result in an error.
You request a specific version when opening a database:
const request = indexedDB.open("AppDatabase", 2);When this line executes, the browser compares the requested version
with the database’s existing version on disk: - If the database does not
exist, the browser treats the current version as 0 and
fires the upgrade process to reach version 2. - If the
database exists and the requested version is higher than the current
version, an upgrade is triggered. - If the requested version matches the
existing version, the database opens normally via the
onsuccess event. - If the requested version is lower, an
onerror event is fired with a
VersionError.
The
onupgradeneeded Event and versionchange
Transaction
Structural modifications—such as calling
createObjectStore, deleteObjectStore,
createIndex, or deleteIndex—cannot be
performed during standard read or write operations. They are only
permitted inside the onupgradeneeded event handler.
When onupgradeneeded fires, the browser automatically
creates an exclusive, system-managed transaction of type
versionchange. Key characteristics of this transaction
include:
- Exclusive Lock: It blocks all other connections and transactions across all browser tabs for that database until the upgrade finishes or fails.
- Atomicity: If an unhandled exception occurs or
transaction.abort()is called, all changes made during the upgrade are rolled back, and the database version reverts to its previous state. - Data Migration Access: The
versionchangetransaction allows both schema updates and standard data read/write operations, making it possible to migrate or transform existing records during the upgrade.
Managing Incremental Migrations
The IDBVersionChangeEvent object passed to
onupgradeneeded provides two useful properties:
event.oldVersion and event.newVersion.
To handle users upgrading from different older versions, use
conditional logic (such as a switch statement without
break statements, or simple if checks) to
apply migrations sequentially:
const request = indexedDB.open("AppDatabase", 3);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
// Version 1: Initial setup
if (oldVersion < 1) {
const userStore = db.createObjectStore("users", { keyPath: "id" });
userStore.createIndex("by_email", "email", { unique: true });
}
// Version 2: Add a new index to the existing store
if (oldVersion < 2) {
const transaction = event.target.transaction;
const userStore = transaction.objectStore("users");
userStore.createIndex("by_name", "name", { unique: false });
}
// Version 3: Create an entirely new store
if (oldVersion < 3) {
db.createObjectStore("settings", { keyPath: "key" });
}
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log("Database opened successfully at version:", db.version);
};
request.onerror = (event) => {
console.error("Database error:", event.target.error);
};Handling Blocked Upgrades and Multi-Tab Conflicts
Because an upgrade transaction requires exclusive access to the database, an upgrade cannot proceed if another tab or window has an open connection to an older version of the same database.
IndexedDB provides two events to manage this scenario:
onblocked(on the opening request): Fires when an upgrade attempt is waiting for older database connections in other tabs to close.onversionchange(on the open database instance): Fires on existing connections in other tabs, notifying them that an upgrade has been requested elsewhere.
To ensure smooth upgrades across multiple tabs, always listen for
onversionchange on your database instances and close them
promptly:
request.onsuccess = (event) => {
const db = event.target.result;
db.onversionchange = () => {
db.close();
alert("A new version of the app is available. Please reload the page.");
};
};
request.onblocked = () => {
console.warn("Upgrade blocked: Please close other tabs running this app.");
};