IndexedDB is useful for storing structured data in the browser. Applications use it for offline access, cached API responses, user preferences, drafts, and other information that must remain available between sessions.
As an application evolves, its database structure may need to change. You might add a new object store, introduce an index, or update how records are organized. IndexedDB supports these changes through database version upgrades.
However, an upgrade can become blocked when another browser tab still has an older connection open.
For example, a user opens an application in two tabs. The first tab remains open in the background, while the second tab loads a newer version of the application that requires a database schema update. The new version requests an upgrade, but the older tab continues using the existing database connection. The browser waits for that connection to close before allowing the upgrade to proceed.
This behavior protects the database from incompatible schema changes. The challenge for developers is to handle it correctly so users do not experience endless loading screens or unexpected failures.
How IndexedDB Database Versioning Works
IndexedDB databases have a version number. When an application opens a database without specifying a version, the browser opens it using the existing version or creates it with the default version if it does not exist.
When a specific version is supplied, the browser compares it with the current database version.
const request = indexedDB.open("MyApplicationDB", 2);
If the database does not exist, IndexedDB creates it at version 2.
If the existing database has version 1, IndexedDB attempts to upgrade it to version 2.
If the existing database already has version 2, the browser opens it without running the upgrade handler.
A request to open a lower version than the existing database version fails with a VersionError.
Using onupgradeneeded for schema changes
The onupgradeneeded event is where database schema changes should be performed. It runs when the database is created or when its version increases.
For example, an application might initially store customer records in an object store and later need an index for searching by email address.
const request = indexedDB.open("MyApplicationDB", 2);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains("customers")) {
db.createObjectStore("customers", {
keyPath: "id"
});
}
const transaction = event.target.transaction;
const customers = transaction.objectStore("customers");
if (!customers.indexNames.contains("email")) {
customers.createIndex("email", "email", {
unique: true
});
}
};
The example creates the customers object store if necessary and adds an index when it is missing.
In a real application, schema changes should be organized according to the previous database version. Checking existing stores and indexes is useful, but it does not replace proper migration planning.
Why Another Browser Tab Blocks an Upgrade
An IndexedDB upgrade requires exclusive access to the database. Existing connections must release their access before the browser can apply the new schema.
Consider this scenario:
- A user opens the application in Tab A.
- Tab A opens the database at version 1.
- The application is deployed with code that requests version 2.
- The user opens the updated application in Tab B.
- Tab B requests the database at version 2.
- Tab A still has its version 1 connection open.
- The upgrade waits until Tab A closes its connection.
The browser does not automatically terminate every older connection because doing so could interrupt transactions or leave application code in an unexpected state.
This behavior becomes particularly noticeable in applications that remain open for hours, such as administrative dashboards, project management tools, email clients, and offline-first web applications.
The versionchange event
When another connection requests a database version change, existing connections receive the versionchange event.
Applications should use this event to close their database connections.
const request = indexedDB.open("MyApplicationDB", 1);
request.onsuccess = (event) => {
const db = event.target.result;
db.onversionchange = () => {
db.close();
console.log("Database connection closed for an upgrade.");
};
};
Calling db.close() prevents the connection from accepting new transactions. Transactions already in progress are allowed to finish before the connection is fully closed.
Once all blocking connections have released the database, the pending upgrade can proceed.
The versionchange event is therefore an important part of database connection management, not just an optional notification.
Understanding the blocked Event
The blocked event is raised on the request that is trying to open or upgrade the database when another connection prevents the operation from proceeding.
For example:
const request = indexedDB.open("MyApplicationDB", 2);
request.onblocked = () => {
console.warn(
"The database upgrade is waiting for another tab to close its connection."
);
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log("Database opened at version:", db.version);
};
request.onerror = () => {
console.error("Unable to open the database:", request.error);
};
A blocked request has not necessarily failed. The browser can continue waiting, and the request may succeed after the other connection closes.
This distinction matters when building user interfaces. Treating blocked as a permanent failure can cause unnecessary error messages or force users to restart an application that could recover automatically.
A better approach is to notify the user that another open tab needs to release the database connection.
Building a More Reliable IndexedDB Opening Function
A production application should handle database creation, schema upgrades, blocking connections, successful opening, and errors in one predictable place.
The following example uses a reusable function that creates the database and closes existing connections when another tab requests an upgrade.
function openApplicationDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("MyApplicationDB", 2);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const transaction = event.target.transaction;
if (!db.objectStoreNames.contains("customers")) {
db.createObjectStore("customers", {
keyPath: "id"
});
}
const customers = transaction.objectStore("customers");
if (!customers.indexNames.contains("email")) {
customers.createIndex("email", "email", {
unique: true
});
}
};
request.onblocked = () => {
console.warn(
"Database upgrade blocked. Close other application tabs."
);
// Notify the user through the application's UI if needed.
};
request.onerror = () => {
reject(request.error ?? new Error("Unable to open database."));
};
request.onsuccess = (event) => {
const db = event.target.result;
db.onversionchange = () => {
db.close();
// Prevent the application from continuing to use
// a connection that is no longer compatible.
console.warn(
"Database connection closed because its version changed."
);
};
resolve(db);
};
});
}
You can use the function like this:
async function initializeApplication() {
try {
const db = await openApplicationDatabase();
console.log("Database ready:", db.name);
// Initialize application features that use IndexedDB.
// Keep the database connection available while needed.
} catch (error) {
console.error("Database initialization failed:", error);
}
}
initializeApplication();
This implementation handles the most important connection lifecycle events. The onblocked handler logs a message, but a production application should usually show a visible notification when the delay affects the user.
For example, the interface might display: "Your data storage is being updated. Close other tabs of this application if the update does not finish."
The message should not claim that the upgrade has failed unless the request actually reports an error.
Common Mistakes That Cause Upgrade Problems
Keeping database connections open indefinitely
An application may open IndexedDB during initialization and never close the connection. That is not automatically wrong, but the application must respond to version changes.
Attach an onversionchange handler to every connection so that older tabs can release the database when an upgrade is requested.
Assuming the blocked event means failure
The blocked event indicates that the operation is waiting for another connection. It is not the same as the request's error event.
Keep the request active and allow it to complete if the blocking connection closes.
Opening multiple connections without managing them
Different application modules may independently call indexedDB.open(). If each module manages its connection separately, it becomes difficult to determine which connection is preventing an upgrade.
Use a shared database service or another consistent connection-management strategy. Every connection should have a version-change handler.
Performing long-running work during an upgrade
The upgrade transaction should be used for schema changes and necessary data migrations. Do not attempt to perform network requests, wait for timers, or run unrelated asynchronous operations inside it.
An IndexedDB upgrade transaction is transaction-scoped. If the transaction becomes inactive or is aborted, the schema changes are rolled back.
For large data migrations, plan carefully. Some record transformations may need to happen within the upgrade transaction, while more extensive processing may be better handled through a separate, explicitly designed migration strategy.
Forgetting that old application code may still be running
Closing a connection allows an upgrade to proceed, but it does not automatically update the JavaScript already loaded in another tab.
If older code continues running after its connection closes, it may attempt to use a closed database connection or make assumptions about the previous schema.
Applications should handle the connection's closure gracefully. Depending on the use case, they can prompt the user to reload the page or reinitialize affected features after the upgrade.
How to Debug a Blocked IndexedDB Upgrade
When a database upgrade appears stuck, follow these steps.
- Check the requested database version. Confirm that the new application is requesting the intended version and that the existing database is not already newer.
- Add logging to
onblocked, onupgradeneeded, onsuccess, and onerror. This helps distinguish a waiting request from a failed request. - Inspect other open tabs and windows. Look for older application instances that may still hold a database connection.
- Confirm that every connection registers
db.onversionchange. A missing handler is a common reason an upgrade remains blocked until the user closes an old tab. - Check database service code for repeated
indexedDB.open() calls. Multiple connections may exist even if the application appears to use a single database service. - Test the upgrade with two tabs. Open the old version in one tab, then load the new version in another. Confirm that the old connection closes and the upgrade completes.
- Test failure paths. Verify that an aborted upgrade or an unsuccessful open request produces a useful error message and does not leave the application in a broken initialization state.
Browser developer tools can help inspect IndexedDB databases and their stored data. However, clearing the database should not be the first troubleshooting step because it may delete locally stored user information.
Best Practices for IndexedDB Schema Upgrades
Practice | Why it matters |
|---|
Increase the database version only when the schema changes | Avoids unnecessary upgrade transactions |
Handle versionchange on every connection | Allows other tabs to upgrade the database |
Handle blocked separately from error | Prevents false failure messages |
Keep upgrade logic focused | Reduces the risk of transaction failures |
Make schema migrations safe to execute against the expected previous version | Helps existing users upgrade reliably |
Centralize connection management where practical | Makes it easier to find and close active connections |
Test upgrades with multiple tabs open | Reproduces a common real-world failure |
Provide a clear reload or close-tab message when necessary | Helps users recover without guessing |
Another important practice is to treat local database upgrades as part of application deployment. A new release may reach users while old tabs remain open, so the migration strategy should account for mixed application versions.
Advantages and Limitations
Handling IndexedDB connections correctly offers several benefits:
- Schema upgrades can proceed without requiring users to close every tab manually.
- Database operations become easier to troubleshoot.
- Applications can explain upgrade delays instead of appearing frozen.
- Connection lifecycle behavior becomes more predictable.
There are also limitations. An application cannot force every other tab to close immediately, and closing a connection does not update the older tab's JavaScript. Users may still need to reload an outdated page, particularly when the new release changes how stored data is accessed.
For applications with important offline data, migration and recovery behavior should be tested before deployment.
Summary
IndexedDB upgrades become blocked when another open connection is still using the database. This is expected browser behavior that protects existing transactions and prevents incompatible schema changes.
The most important fix is to register a versionchange handler on every database connection and close the connection when an upgrade is requested. On the upgrading request, handle blocked separately from error, keep the request active, and provide a clear message if the user needs to close another tab.
With centralized connection management, focused upgrade transactions, and multi-tab testing, IndexedDB schema changes become much more reliable for real-world applications.
Join the conversation! Your thoughts help the community grow.