Fix: IndexedDB Not Working — Transaction Inactive, Upgrade Blocked, or Store Not Found
Quick Answer
How to fix IndexedDB issues — transaction lifecycle, version upgrades, blocked events, cursor iteration, IDBKeyRange queries, and using idb wrapper library to avoid callback hell.
The Problem
An IndexedDB write fails with TransactionInactiveError:
const request = indexedDB.open('mydb', 1);
request.onsuccess = (e) => {
const db = e.target.result;
setTimeout(() => {
const tx = db.transaction('users', 'readwrite');
tx.objectStore('users').add({ id: 1, name: 'Alice' });
// DOMException: The transaction has finished.
}, 100);
};Or the database upgrade is blocked and onupgradeneeded never fires:
const request = indexedDB.open('mydb', 2);
request.onupgradeneeded = (e) => { /* Never called */ };
request.onsuccess = (e) => { /* Also never called */ };
// request.onblocked fires insteadOr an object store doesn’t exist despite being created in onupgradeneeded:
const tx = db.transaction('products', 'readonly');
// DOMException: No objectStore named products in this transactionWhy This Happens
IndexedDB has a unique transactional model that trips up most developers:
- Transactions auto-commit when idle — a transaction is automatically committed when no pending requests remain in the current event loop tick. You cannot resume a transaction after an
await, asetTimeout, or any async gap. - Schema changes only happen in
onupgradeneeded— object stores and indexes can only be created or deleted during a version upgrade. Attempting to create a store in a regular transaction throws. - Version upgrades are blocked by other open connections — if another tab has the same database open at the old version,
onupgradeneededwon’t fire until all other connections close (or callclose()). This causes theblockedevent. - Store names must match exactly —
db.transaction('Users', 'readonly')anddb.transaction('users', 'readonly')are different. The store name is case-sensitive.
Fix 1: Never Use Transactions Across Async Gaps
Keep all transaction operations synchronous or chained through IDB request events — never through await or setTimeout:
// WRONG — transaction is committed before the await resolves
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const userRequest = store.get(1);
userRequest.onsuccess = async () => {
const user = userRequest.result;
await someAsyncOperation(); // Transaction auto-commits here!
store.put({ ...user, name: 'Bob' }); // TransactionInactiveError
};
// CORRECT — chain operations through IDB request events, no async gaps
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const getRequest = store.get(1);
getRequest.onsuccess = () => {
const user = getRequest.result;
const putRequest = store.put({ ...user, name: 'Bob' });
putRequest.onsuccess = () => {
console.log('Updated successfully');
};
};
tx.oncomplete = () => console.log('Transaction committed');
tx.onerror = () => console.error('Transaction failed:', tx.error);Best practice: use the idb library to wrap IndexedDB with Promises:
import { openDB } from 'idb';
const db = await openDB('mydb', 1, {
upgrade(db) {
db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
}
});
// idb keeps transactions alive across awaits within the same tick
// but you still can't span real async operations
async function updateUser(id, newName) {
const tx = db.transaction('users', 'readwrite');
const user = await tx.store.get(id); // OK — idb chains these
await tx.store.put({ ...user, name: newName });
await tx.done; // Wait for commit
}
// Clean reads and writes
const user = await db.get('users', 1);
await db.put('users', { ...user, name: 'Bob' });
await db.delete('users', 2);
const all = await db.getAll('users');Fix 2: Set Up Schema in onupgradeneeded
All object store and index creation must happen inside onupgradeneeded:
const request = indexedDB.open('mydb', 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
const oldVersion = e.oldVersion;
// Run migration steps based on version
if (oldVersion < 1) {
// Version 1: create initial schema
const userStore = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
userStore.createIndex('by_email', 'email', { unique: true });
userStore.createIndex('by_name', 'name', { unique: false });
}
if (oldVersion < 2) {
// Version 2: add products store
const productStore = db.createObjectStore('products', { keyPath: 'sku' });
productStore.createIndex('by_category', 'category');
productStore.createIndex('by_price', 'price');
}
if (oldVersion < 3) {
// Version 3: add compound index to existing store
// Access existing store via the upgrade transaction
const userStore = e.target.transaction.objectStore('users');
userStore.createIndex('by_name_email', ['name', 'email'], { unique: true });
}
};
request.onsuccess = (e) => {
const db = e.target.result;
// At this point, all stores from onupgradeneeded are available
};
request.onerror = (e) => {
console.error('Failed to open database:', e.target.error);
};Using idb for migrations:
import { openDB } from 'idb';
const db = await openDB('mydb', 3, {
upgrade(db, oldVersion, newVersion, transaction) {
if (oldVersion < 1) {
const users = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
users.createIndex('by_email', 'email', { unique: true });
}
if (oldVersion < 2) {
db.createObjectStore('products', { keyPath: 'sku' });
}
if (oldVersion < 3) {
const users = transaction.objectStore('users');
users.createIndex('by_role', 'role');
}
},
blocked(currentVersion, blockedVersion, event) {
// Old version has open connections — tell user to close other tabs
alert('Please close other tabs with this app open to upgrade the database.');
},
blocking(currentVersion, newVersion, event) {
// This tab is blocking an upgrade in another tab
db.close(); // Close our connection to unblock the upgrade
},
});Fix 3: Handle Version Blocked Events
When another tab has the database open, upgrades are blocked:
const request = indexedDB.open('mydb', 2);
request.onblocked = (e) => {
// Another connection is open and not calling db.close()
console.warn('Database upgrade blocked. Close other tabs.');
// Optional: notify user
document.querySelector('#upgrade-notice').style.display = 'block';
};
request.onupgradeneeded = (e) => {
// Only runs after all blocking connections close
const db = e.target.result;
// ... migration code
};
// In the existing connection — listen for version change requests
const existingDb = /* ... */;
existingDb.onversionchange = () => {
// Another tab wants to upgrade — close our connection
existingDb.close();
// Optionally reload the page to pick up the new schema
window.location.reload();
};Fix 4: Query with Indexes and IDBKeyRange
Retrieve specific records efficiently using indexes and key ranges:
// Open database (schema: users store with 'by_email' and 'by_name' indexes)
const db = await openDB('mydb', 1);
// Get by primary key
const user = await db.get('users', 1);
// Get all records
const allUsers = await db.getAll('users');
// Get by index value
const userByEmail = await db.getFromIndex('users', 'by_email', '[email protected]');
// Get all matching an index value
const smiths = await db.getAllFromIndex('users', 'by_name', 'Smith');
// Range queries with IDBKeyRange
const IDBKeyRange = window.IDBKeyRange;
// Users with id between 10 and 20 (inclusive)
const range1 = IDBKeyRange.bound(10, 20);
const batch = await db.getAll('users', range1);
// Users with id > 5 (exclusive lower bound)
const range2 = IDBKeyRange.lowerBound(5, true);
const afterFive = await db.getAll('users', range2, 10); // Limit to 10 results
// Find users by name prefix (e.g., all names starting with 'Al')
const range3 = IDBKeyRange.bound('Al', 'Al\uffff'); // \uffff is a high character
const alNames = await db.getAllFromIndex('users', 'by_name', range3);Cursor-based iteration for large datasets:
import { openDB } from 'idb';
const db = await openDB('mydb', 1);
// Iterate through all users with a cursor
const tx = db.transaction('users', 'readwrite');
let cursor = await tx.store.openCursor();
while (cursor) {
const user = cursor.value;
if (user.inactive) {
await cursor.delete(); // Delete inactive users
} else {
await cursor.update({ ...user, lastChecked: new Date() });
}
cursor = await cursor.continue();
}
await tx.done;
// Iterate an index with a range
const nameTx = db.transaction('users', 'readonly');
const index = nameTx.store.index('by_name');
let nameCursor = await index.openCursor(IDBKeyRange.lowerBound('M'));
const mNames = [];
while (nameCursor) {
mNames.push(nameCursor.value);
nameCursor = await nameCursor.continue();
}Fix 5: Batch Operations for Performance
Single-record operations have high overhead due to transaction setup. Batch writes in a single transaction:
import { openDB } from 'idb';
const db = await openDB('mydb', 1);
// SLOW — one transaction per record
for (const user of users) {
await db.put('users', user); // New transaction each time
}
// FAST — one transaction for all records
async function bulkInsert(users) {
const tx = db.transaction('users', 'readwrite');
await Promise.all([
...users.map(user => tx.store.put(user)),
tx.done
]);
}
// Bulk insert 10,000 records efficiently
await bulkInsert(largeUserArray);
// Clear and repopulate
async function replaceAll(newUsers) {
const tx = db.transaction('users', 'readwrite');
await tx.store.clear();
await Promise.all(newUsers.map(u => tx.store.put(u)));
await tx.done;
}Fix 6: Debug IndexedDB in DevTools
Chrome and Firefox DevTools have built-in IndexedDB viewers:
// Log all data in a store for debugging
async function dumpStore(dbName, storeName) {
const db = await openDB(dbName);
const all = await db.getAll(storeName);
console.table(all);
db.close();
}
// Delete the entire database to start fresh
async function resetDatabase(dbName) {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName);
request.onsuccess = resolve;
request.onerror = reject;
request.onblocked = () => console.warn('Delete blocked — close other tabs');
});
}
// Check what databases exist (Chrome 73+)
const databases = await indexedDB.databases();
console.table(databases);
// [{ name: 'mydb', version: 3 }, ...]
// Check current version
const request = indexedDB.open('mydb');
request.onsuccess = (e) => {
console.log('Current version:', e.target.result.version);
e.target.result.close();
};Still Not Working?
Private/incognito mode has storage restrictions — in Safari private mode, IndexedDB throws errors for any write operation (QuotaExceededError or InvalidStateError). Always wrap IndexedDB calls in try/catch and fall back to in-memory storage if needed. Firefox private mode allows IndexedDB but clears it when the window closes.
Storage quota exceeded — browsers allocate IndexedDB storage based on available disk space (typically 20-60% of available disk for the origin group). Large datasets can hit this limit. Use navigator.storage.estimate() to check:
const estimate = await navigator.storage.estimate();
console.log(`Used: ${estimate.usage} bytes`);
console.log(`Available: ${estimate.quota} bytes`);Request persistent storage to avoid eviction under storage pressure:
if (await navigator.storage.persist()) {
console.log('Storage will not be evicted');
}Cross-origin iframes have separate IndexedDB — each origin has its own IndexedDB namespace. Data stored by https://app.example.com is not accessible to an iframe on https://widget.example.com, even within the same parent page.
iOS Safari wipes IndexedDB on low storage — Safari on iOS can delete IndexedDB data without warning when the device storage is low. Don’t use IndexedDB as the sole source of truth for critical data; sync to a server when online.
For related browser storage issues, see Fix: Vite Env Variables Not Working and Fix: JavaScript Unhandled Promise Rejection.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Web Worker Not Working — postMessage Ignored, Cannot Import Module, or Worker Crashes Silently
How to fix Web Worker issues — postMessage data cloning, module workers, error handling, SharedArrayBuffer setup, Comlink, and common reasons workers silently fail.
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.