Repository navigation
[Due for payment 2026-04-14] [$250] [Sentry] fix The database connection is closing issue #84192
Description
Sentry issue https://expensify.sentry.io/issues/7092239780
Thread https://expensify.slack.com/archives/C05LX9D6E07/p1770496102994319
Please re-state the problem that we are trying to solve in this issue.
When Onyx operations attempt to create an IndexedDB transaction, the browser may have already closed the database connection, causing:
InvalidStateError: Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing.
This affects ~58,700 occurrences across 430 users on all browsers (Chrome: 31K, Safari: 18K, Firefox: 5K, Mobile Safari: 3K, Edge: 1K). The error is an unhandled promise rejection.
What is the root cause of that problem?
The existing db.onclose handler (line 22) sets dbp = undefined when the browser closes the connection, which protects future calls to getDB(). However, any operations that have already resolved the dbp promise still hold a stale db reference and proceed to call db.transaction() at line 59 — which throws InvalidStateError on the now-closing connection.
This can be triggered by:
• The browser closing idle IDB connections (Safari is especially aggressive, but Chrome/Firefox do this too under memory pressure or tab suspension)
• Another tab triggering a version upgrade (versionchange event — currently unhandled)
• The verifyStoreExists path closing the DB (line 39) while concurrent operations still hold the old reference
What changes do you think we should make in order to solve the problem?
All changes are in lib/storage/providers/IDBKeyValProvider/createStore.ts:
-
Retry mechanism with a single retry on InvalidStateError
— Extract the transaction chain (getDB → verifyStoreExists → db.transaction) into openTransactionAndExecute()
— Wrap the call in .catch(): if the error is InvalidStateError, reset dbp = undefined and retry once with a fresh connection
— If the retry also fails, the error is thrown normally and still surfaces in Sentry exactly as it does today -
Diagnostic logging on every retry (does NOT hide the issue)
— Log at alert level with: dbName, storeName, txMode, and closedBy
— closedBy tracks why the connection was closed: 'browser' (user-agent closed it), 'versionchange' (cross-tab upgrade), 'verifyStoreExists' (missing store path), or 'unknown'
— This gives us more visibility than today (where we only get a crash count with no context) -
onversionchange handler
— When another tab triggers a DB version upgrade, proactively close the connection and reset dbp
— Without this, cross-tab upgrades can block indefinitely and in-flight operations crash on the stale connection -
onclose handler now logs
— The existing onclose handler now logs when the browser unexpectedly closes the connection, giving us data on browser-initiated closures even when no retry is needed
Key safety guarantees:
• Data is never lost — the transaction fails before any data is written; the retry completes successfully with a valid connection
• Only one retry is attempted — no infinite loops
• If the retry fails, the error propagates normally and Sentry captures it as before
• Normal operations (no InvalidStateError) are completely unaffected — zero performance impact
• The closedBy logging gives us instrumentation to continue investigating the root cause of the spike
cc @fabioh8010
Issue Owner
Current Issue Owner: @mallenexpensifyUpwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022029317821194916452
- Upwork Job ID: 2029317821194916452
- Last Price Increase: 2026-03-04
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
MelvinBot commentedon Mar 4, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When Onyx operations attempt to create an IndexedDB transaction, the browser may have already closed the database connection, causing
InvalidStateError: Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing. This affects ~58,700 occurrences across 430 users on all browsers (Chrome: 31K, Safari: 18K, Firefox: 5K, Mobile Safari: 3K, Edge: 1K). The error is an unhandled promise rejection that currently has zero error handling.What is the root cause of that problem?
There are two race conditions in
react-native-onyx'screateStore.js, both converging on the same unprotecteddb.transaction()call at the return statement:Race Condition A (primary — ~58K occurrences):
getDB()returns the cacheddbppromise which resolves to adbobject. Between the promise resolving anddb.transaction()executing, the browser closes the connection (Safari does this aggressively for idle connections; Chrome/Firefox do it under memory pressure or tab suspension). Thedb.onclosehandler fires and setsdbp = undefined, which helps future calls togetDB()re-open the connection — but the in-flight.then()chain already has the staledbreference.db.transaction()throwsInvalidStateErrorsynchronously inside the.then()callback.Race Condition B (secondary): The
verifyStoreExistsfunction (added in PR #684) callsdb.close()when it detects the store is missing and needs to recreate it with a version bump. This immediately puts the connection into "closing" state. Any concurrent operation that already resolvedgetDB()with this samedbobject will hitInvalidStateErrorwhen it reachesdb.transaction().Why the error is unhandled: All 9 storage operations (7 via idb-keyval, 2 direct calls in
multiMerge/multiSet) flow through the same single choke point. None of them have any try/catch or error handling arounddb.transaction(). ThetryOrDegradePerformancewrapper instorage/index.jsonly catches synchronous throws — sincedb.transaction()is called inside an async.then()chain, theInvalidStateErrorbecomes a rejected promise that bypasses the try/catch entirely. Additionally,tryOrDegradePerformanceonly checks for"Internal error opening backing store", notInvalidStateError.There is also no
onversionchangehandler, meaning cross-tab database upgrades can forcibly close connections and trigger the same error.What changes do you think we should make in order to solve the problem?
All changes in
react-native-onyx'slib/storage/providers/IDBKeyValProvider/createStore.ts:Wrap
db.transaction()with retry onInvalidStateError— Extract the transaction chain into a helper. Ifdb.transaction()throwsInvalidStateError, resetdbp = undefinedand retry once with a fresh connection. If the retry also fails, propagate the error normally (Sentry still captures it). This is the standard approach used by other libraries facing the same issue (localForage, Firebase, Workbox).Add
onversionchangehandler — When another tab triggers a DB version upgrade, proactively close the connection and resetdbpbefore the browser force-closes it. This prevents the race window where in-flight operations use a connection that's about to be killed.Add diagnostic logging on retry — Log
dbName,storeName,txMode, and the reason the connection was closed (browser,versionchange,verifyStoreExists, orunknown). This gives visibility into which trigger is most common without hiding the error.Enhance
onclosehandler with logging — The existingonclosehandler silently resetsdbp. Adding a log line gives production visibility into browser-initiated closures.What alternative solutions did you explore? (Optional)
MemoryOnlyProvider: Too aggressive — the IDB connection can be re-established with a single retry. Degrading to memory-only loses all persistent state.db.closePendingbeforedb.transaction(): IndexedDB doesn't expose a reliable connection state property — the only way to know the connection is closing is to catch the error.Detailed Investigation
Evidence Chain
The single choke point —
createStore.jsline 83:db.transaction(storeName, txMode)is called inside a.then()with zero error handling. Every storage operation flows through this line.Stale reference problem —
db.onclose(line 54) setsdbp = undefinedfor future callers, but the promise.then()chain already has the resolveddbobject. The staledbreference is used to calldb.transaction(), which throws.No upstream protection — idb-keyval v6.2.1 has the identical unprotected pattern. Its functions (
get,set,del,getMany,delMany,clear,keys) simply callcustomStore(txMode, callback)with no try/catch.tryOrDegradePerformancedoesn't help — It wrapsfn()in try/catch, butfn()returns a promise. TheInvalidStateErroris thrown inside an async.then()chain, becoming a rejected promise that the synchronous try/catch cannot intercept. The error message also doesn't match either of the two checked patterns.verifyStoreExistssecondary race — Added in PR #684 (merged Oct 2025) to handle corrupted databases from third-party scripts. When the store is missing, it callsdb.close()and reopens with a version bump. Any concurrent operation holding the olddbreference will fail.Missing
onversionchange— No handler exists anywhere in the codebase. Cross-tab upgrades (including those triggered byverifyStoreExists) can force-close connections on other tabs.Relevant Code
createStore.js:81-83— The unprotecteddb.transaction()call (single choke point)createStore.js:50-54—db.onclosehandler that only helps future callscreateStore.js:62-79—verifyStoreExistswithdb.close()racestorage/index.js:62-85—tryOrDegradePerformancethat doesn't catch this erroridb-keyval/dist/index.cjs:32-46— Upstream functions with zero error handlingOpen Questions
verifyStoreExistsactually find the store missing? If rarely, Race Condition A (browser-initiated closures) dominates.onversionchangehandler is critical.Next Steps for Contributor+ team: Reply with
@MelvinBot implement thisto create a draft PR, or@MelvinBot <your feedback>to refine this analysis.melvin-bot commentedon Mar 4, 2026
Current assignee @Krishna2323 is eligible for the External assigner, not assigning anyone new.
[-][Sentry] fix The database connection is closing issue[/-][+][$250] [Sentry] fix The database connection is closing issue[/+]melvin-bot commentedon Mar 4, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022029317821194916452
melvin-bot commentedon Mar 10, 2026
@Krishna2323 Huh... This is 4 days overdue. Who can take care of this?
Krishna2323 commentedon Mar 10, 2026
Not overdue, draft PR is up. Will open for review shortly.
41 remaining items
melvin-bot commentedon Apr 14, 2026
Payment Summary
Upwork Job
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Apr 15, 2026
Is this due payment or part of a project? Plz tag me if it's due.
Krishna2323 commentedon Apr 15, 2026
@mallenexpensify it's due payment. Checklist not needed.
mallenexpensify commentedon Apr 16, 2026
Payment Summary
Contributor+: @Krishna2323 due $250 via NewDot
Contributor+: @mkhutornyi due $250 via NewDot
Krishna2323 commentedon Apr 17, 2026
Requested in ND.
laurenreidexpensify commentedon Apr 20, 2026
@Krishna2323 $250 approved in New Expensify
trjExpensify commentedon May 6, 2026
approved $250 for @mkhutornyi