Fix dead-code async-catch in tryOrDegradePerformance App#784
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd82e39dc9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
I have read the CLA Document and I hereby sign the CLA |
|
@elirangoshen I will review soon, but meanwhile:
|
|
@elirangoshen could you try with just |
|
|
Sure I added |
did but still have error there |
fabioh8010
left a comment
There was a problem hiding this comment.
@elirangoshen Please check #784 (comment)
Per Expensify#784 (comment), switch from `Promise.resolve(fn()).catch(...)` to `.then(() => fn()).catch(...)` so synchronous throws from `fn` are also routed through the degrade-detection catch handler. Add two test cases covering the sync-throw paths.
Picks up the sync-throw handling fix in Expensify/react-native-onyx#784 (comment).
fixed, please check again |
Details
tryOrDegradePerformanceinlib/storage/index.tswas supposed to detect known critical IndexedDB failures and gracefully fall back to an in-memory storage provider viadegradePerformance. It wrapped every storage call in a synchronoustry/catch— but every storage provider method is async and the helper didresolve(fn())(noawait). Any rejection fromfn()propagated through the promise chain and never re-entered thecatch. Result: the fallback branch has been dead code, and theLogger.logHmmm("Falling back to only using cache and dropping storage…")message has never fired in production.This PR fixes it by replacing the synchronous wrapper with a
.then(() => fn()).catch(...)chain so the helper handles both sync throws and async rejections fromfnuniformly:Notes on the chosen shape:
initialization.then(() => fn())runsfninside a.thencallback. Iffnthrows synchronously, the.thenreturns a rejected promise; if it returns a value/promise,.thenfollows the chain. Either way, the trailing.catchsees the error. This matches the original helper's intent — be a uniform safety net regardless of whetherfnhappens to throw sync or reject async — addressing the Codex review concern and the follow-up from @fabioh8010.While here, the
'Internal error opening backing store for indexedDB.open'branch has been removed from this function. Per the parent investigation in Expensify/App#87862 (comment), that error class indicates permanent IndexedDB corruption that a provider swap toMemoryOnlyProvidercannot recover from — it will be routed to a dedicated heal path in Expensify/App#90636. Leaving the check here would conflict with that heal mechanism. The'IDBKeyVal store could not be created'branch stays — that's an init-time failure where falling back to memory is still the correct behavior.Related Issues
Expensify/App#90632
Automated Tests
Added
tests/unit/storage/tryOrDegradePerformanceTest.tswith four cases that exercise the storage module via its public API (sincetryOrDegradePerformanceitself isn't exported):Promise.reject(new Error('IDBKeyVal store could not be created')), calls a storage op, and asserts: (a) the promise rejects with that error, (b)Logger.logHmmmwas called with the "Falling back to only using cache…" message — provingdegradePerformanceran, (c)storage.getStorageProvider().name === 'MemoryOnlyProvider'.new Error('Some unrelated storage failure'). Asserts the rejection propagates,Logger.logHmmmis not called, and the active provider stays the same.throw(not reject) the target error. Asserts the same three outcomes as case (1), proving the new.then(() => fn()).catch(...)form catches synchronous throws as well.All four use
jest.isolateModulesto load a freshlib/storageper case so the module-privateprovidervariable doesn't leak between tests, andjest.unmock('../../../lib/storage')to bypass the global mock applied byjestSetup.js.Without the fix in this PR, cases (1) and (2) fail — the async rejection escapes the old synchronous
catchblock anddegradePerformanceis never called.Full test suite (
npm test) passes: 17 suites / 453 tests. Typecheck (npm run typecheck) and lint on the changed files are clean.Manual Tests
Tested end-to-end against Expensify/App in this draft PR: Expensify/App#90768 (bumps
react-native-onyxto this branch's head commit via thegit+https://…#<sha>hash trick inpackage.json).The fix targets the IndexedDB code path. IndexedDB is web-only — mobile uses
SQLiteProviderand never reaches the changed branch — so the meaningful manual tests are all on web; mobile is purely a non-regression check.npm install,npm run web. Confirm the App boots and behaves identically tomainon the happy path: log in, view reports, open a chat, send a message, refresh.Logger.logHmmm("Falling back to only using cache and dropping storage")is not logged during normal use.IDBKeyValProvider.setItem(or any other provider method) innode_modules/react-native-onyx/dist/storage/providers/IDBKeyValProvider.js. When the breakpoint hits, throw/rejectnew Error('IDBKeyVal store could not be created')from the debugger console.[Onyx] Error while using IDBKeyValProvider. Falling back to only using cache and dropping storage.plus the error stack.main), step 4's log message never appears even though the same underlying IDB error fires — confirming the dead-code bug.main.useOnyx-heavy screens (Reports list, Money Request flows) on web, iOS, and Android — no observable behavior change vs.main.Author Checklist
### Related Issuessection aboveTestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Avataris modified, I verified thatAvataris working as expected in all cases)mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-05-15.at.13.56.18.mov
Screen.Recording.2026-05-15.at.13.58.09.mov