You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
During the initial downstreamResyncOnce pull (first-time bulk sync from server), the premium IndexedDB WAL storage emits a flood of Uncaught (in promise) TransactionInactiveError in the browser console. The operations self-heal via the addTask retry path, so no data is lost, but the rejected promises from abandoned optimistic calls are never caught, causing noisy unhandled rejections. The issue is present in both multiInstance: true and (less frequently) multiInstance: false configurations.
Environment
rxdb: 17.2.0
rxdb-premium: 17.2.0 (IndexedDB storage with WAL mode)
Browser: Chrome (reproducible consistently on first sync)
getAssumedMasterState in meta-instance.ts (reading replication checkpoint)
Stack Traces (two distinct paths)
Path A — findDocumentsById:
Uncaught (in promise) TransactionInactiveError: Failed to execute 'get' on 'IDBObjectStore': The transaction is not active.
at indexedDBFindByIds (indexeddb-find-by-ids.js:1)
at indexeddb-storage-instance.js:1
at indexeddb-transaction.js:1 ← addTask / IndexedDBTransactionScope
at runWalBranchPredicted (indexeddb-wal.js:1)
at indexeddb-transaction.js:1
at rx-storage-instance-sharding.js:1
at rx-storage-helper.ts:822
at lockedRun (rx-database.ts:520)
at findDocumentsById (rx-storage-helper.ts:821)
at downstream.ts:293
at persistFromMaster (downstream.ts:274)
at downstreamResyncOnce (downstream.ts:199)
Path B — getAssumedMasterState:
Uncaught (in promise) TransactionInactiveError: Failed to execute 'get' on 'IDBObjectStore': The transaction is not active.
at indexedDBFindByIds (indexeddb-find-by-ids.js:1)
at indexeddb-storage-instance.js:1
at indexeddb-transaction.js:1
at runWalBranchPredicted (indexeddb-wal.js:1)
at indexeddb-transaction.js:1
at getAssumedMasterState (meta-instance.ts:114)
at downstream.ts:294
at persistFromMaster (downstream.ts:274)
at downstreamResyncOnce (downstream.ts:199)
Root Cause Analysis
runWalBranchPredicted — three branches, two bugs
// indexeddb-wal.js (minified, expanded for clarity)exportasyncfunctionrunWalBranchPredicted(instance,tx,taskFn){// Branch A if(tx.walDone)returntaskFn();// Branch B — safe sequential path if(instance.multiInstance===false&&instance.internals.isWalDirty===true)returnawaittx.wal,taskFn();// Branch C — optimistic path varoptimistic=taskFn();returnawaittx.wal ? taskFn() : optimistic;}
Bug 1 — Branch C (multiInstance: true): abandoned rejected promise
When multiInstance: true, Branch C is always taken regardless of WAL dirty state.
optimistic = taskFn() — queues IDB read requests on the current transaction tx
await tx.wal resolves truthy (WAL had pending data) → taskFn() is called a second time
The second taskFn() uses the same tx object, which is now committed → TransactionInactiveError
The first optimistic promise resolves successfully (its requests were queued before commit), but is silently dropped — never awaited when the truthy branch
re-runs taskFn()
The addTask wrapper does catch TransactionInactiveError and retries via a new runReadTask call, so the operation eventually succeeds. However, optimistic is a detached rejected promise with no .catch() handler — it surfaces as an unhandled rejection.
Bug 2 — Branch A (multiInstance: false): stale walDone flag after transaction commit
With multiInstance: false, createTx uses isWalDirty at transaction-creation time:
When the background WAL flush (scheduled by writeIndexedDBWal after a 50 ms idle) creates an IndexedDBTransactionScope and begins ensureWalPersisted, a concurrent findDocumentsById call reuses the same openReadonlyTransaction singleton (same tx).
ensureWalPersisted completes and sets tx.walDone = true on that shared tx, then IDB auto-commits it (no more pending requests on the WAL flush side). When the findDocumentsById task's .then() microtask fires and calls runWalBranchPredicted:
Branch A: tx.walDone === true → taskFn() is called immediately on the already-committed tx → TransactionInactiveError
The inconsistency is that walDone is a transaction-level flag (set per tx object by ensureWalPersisted) while isWalDirty is an instance-level flag (checked at createTx time and at runWalBranchPredicted time). A late-arriving task can observe walDone = true on a committed transaction that it had no part in creating.
Expected Behaviour
runWalBranchPredicted should guarantee that taskFn() is always called on an active (not yet committed) transaction, regardless of WAL timing or multiInstance setting. The TransactionInactiveError from the second taskFn() in Branch C and from Branch A's stale walDone check should not surface as unhandled rejections.
Actual Behaviour
During any bulk write burst (initial pull replication, large push batch), many unhandled TransactionInactiveError rejections appear in the browser console. Operations self-heal via the existing addTask retry, but each retry creates a new transaction and adds latency. On a full initial sync of several thousand documents the error can fire dozens to hundreds of times.
Proposed Fix Direction
Branch C: Ensure the abandoned optimistic promise has a no-op .catch() to prevent the unhandled rejection, and open a fresh transaction for the second taskFn() call rather than reusing the committed one:
// instead of: varoptimistic=taskFn();returnawaittx.wal ? taskFn() : optimistic;// consider:varoptimistic=taskFn();optimistic.catch(()=>{});// prevent unhandled rejection if abandoned if(awaittx.wal){// tx is now committed — need a new transaction for the re-run returnrunReadTask(instance,originalCallback);}returnoptimistic;
Branch A: Before calling taskFn() when walDone is true, verify the transaction is still active (or always open a fresh transaction when walDone was set by a concurrent flush rather than by this task's own ensureWalPersisted):
if(tx.walDone){// Only safe to reuse tx if WE triggered ensureWalPersisted and it just finished.// If walDone was set by a concurrent scope, tx may be committed. try{returntaskFn();}catch(e){if(e.name==='TransactionInactiveError')returnrunReadTask(instance,originalCallback);throwe;}}
Alternatively, IndexedDBTransactionScope could track whether ensureWalPersisted was started by this scope, and refuse to share a tx whose WAL flush was initiated by a different concurrent caller.
Additional Notes
The errors are cosmetic from a correctness standpoint — data integrity is preserved by the retry in addTask. However they mask real errors and add unnecessary IDB round-trips.
Switching to multiInstance: false reduces Bug 1 to zero but Bug 2 still fires occasionally during high-write bursts.
Summary
During the initial downstreamResyncOnce pull (first-time bulk sync from server), the premium IndexedDB WAL storage emits a flood of Uncaught (in promise) TransactionInactiveError in the browser console. The operations self-heal via the addTask retry path, so no data is lost, but the rejected promises from abandoned optimistic calls are never caught, causing noisy unhandled rejections. The issue is present in both multiInstance: true and (less frequently) multiInstance: false configurations.
Environment
wrappedKeyCompressionStorage
Reproduction
The errors appear at two call sites within persistFromMaster → downstreamResyncOnce:
Stack Traces (two distinct paths)
Path A — findDocumentsById:
Path B — getAssumedMasterState:
Root Cause Analysis
runWalBranchPredicted — three branches, two bugs
Bug 1 — Branch C (multiInstance: true): abandoned rejected promise
When multiInstance: true, Branch C is always taken regardless of WAL dirty state.
re-runs taskFn()
The addTask wrapper does catch TransactionInactiveError and retries via a new runReadTask call, so the operation eventually succeeds. However, optimistic is a detached rejected promise with no .catch() handler — it surfaces as an unhandled rejection.
Bug 2 — Branch A (multiInstance: false): stale walDone flag after transaction commit
With multiInstance: false, createTx uses isWalDirty at transaction-creation time:
When the background WAL flush (scheduled by writeIndexedDBWal after a 50 ms idle) creates an IndexedDBTransactionScope and begins ensureWalPersisted, a concurrent findDocumentsById call reuses the same openReadonlyTransaction singleton (same tx).
ensureWalPersisted completes and sets tx.walDone = true on that shared tx, then IDB auto-commits it (no more pending requests on the WAL flush side). When the findDocumentsById task's .then() microtask fires and calls runWalBranchPredicted:
The inconsistency is that walDone is a transaction-level flag (set per tx object by ensureWalPersisted) while isWalDirty is an instance-level flag (checked at createTx time and at runWalBranchPredicted time). A late-arriving task can observe walDone = true on a committed transaction that it had no part in creating.
Expected Behaviour
runWalBranchPredicted should guarantee that taskFn() is always called on an active (not yet committed) transaction, regardless of WAL timing or multiInstance setting. The TransactionInactiveError from the second taskFn() in Branch C and from Branch A's stale walDone check should not surface as unhandled rejections.
Actual Behaviour
During any bulk write burst (initial pull replication, large push batch), many unhandled TransactionInactiveError rejections appear in the browser console. Operations self-heal via the existing addTask retry, but each retry creates a new transaction and adds latency. On a full initial sync of several thousand documents the error can fire dozens to hundreds of times.
Proposed Fix Direction
Branch C: Ensure the abandoned optimistic promise has a no-op .catch() to prevent the unhandled rejection, and open a fresh transaction for the second taskFn() call rather than reusing the committed one:
Branch A: Before calling taskFn() when walDone is true, verify the transaction is still active (or always open a fresh transaction when walDone was set by a concurrent flush rather than by this task's own ensureWalPersisted):
Alternatively, IndexedDBTransactionScope could track whether ensureWalPersisted was started by this scope, and refuse to share a tx whose WAL flush was initiated by a different concurrent caller.
Additional Notes
Co-authored by Claude Sonnet 4.6