Skip to content

Fix: IndexedDB Not Working — Transaction Inactive, Upgrade Blocked, or Store Not Found

FixDevs ·

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 instead

Or an object store doesn’t exist despite being created in onupgradeneeded:

const tx = db.transaction('products', 'readonly');
// DOMException: No objectStore named products in this transaction

Why 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, a setTimeout, 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, onupgradeneeded won’t fire until all other connections close (or call close()). This causes the blocked event.
  • Store names must match exactlydb.transaction('Users', 'readonly') and db.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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles