[$175] [Sentry: APP-KT0] Search bulk actions crash reading undefined search results data #103775
Copy link
Copy link
Open
Labels
BugSomething is broken. Auto assigns a BugZero manager.Something is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorReviewingHas a PR in reviewHas a PR in reviewWeeklyKSv2KSv2
Description
Sentry
https://expensify.sentry.io/issues/APP-KT0
Impact (snapshot at filing, 2026-10-12)
- Users (total since first seen): 96
- Users (last 30d, releases 9.5.5 to 9.5.7 only): 88
- Events (30d window): 98
- First seen: 2026-09-08
- Last seen: ongoing
- Platform: web
- App version(s): 9.5.6
- Affected route(s): /search
- Mechanism: generic (unhandled TypeError during render)
Stack trace (top frames, first-party only)
src/hooks/useSearchBulkActions.ts:2920 .map(({reportID}) => currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`])
src/hooks/useSearchBulkActions.ts:2053 useMemo
SearchBulkActionsButton.tsx:106
Suspected cause
currentSearchResults?.data is undefined while selected reports exist, and the code indexes into it without optional chaining on data. Likely fix: use currentSearchResults?.data?.[...].
Reproduction
Unknown. See the events linked from the Sentry issue.
Related
- Possible sibling (not verified): APP-C89, same 'reading report_' pattern on /search/view/:hash
- Prior GH issues: none found
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022109653509046325310
- Upwork Job ID: 2109653509046325310
- Last Price Increase: 2026-10-12
Reactions are currently unavailable
Metadata
Metadata
Labels
BugSomething is broken. Auto assigns a BugZero manager.Something is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorReviewingHas a PR in reviewHas a PR in reviewWeeklyKSv2KSv2
Type
Projects
- StatusShow more project fieldsNo status
Activity
melvin-bot commentedon Oct 12, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @ikevin127 (
External)[-][Sentry: APP-KT0] Search bulk actions crash reading undefined search results data[/-][+][$175] [Sentry: APP-KT0] Search bulk actions crash reading undefined search results data[/+]melvin-bot commentedon Oct 12, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022109653509046325310
haiquywork commentedon Oct 12, 2026
Proposal
What is the root cause of that problem?
In
useSearchBulkActions, theselectedMergeReportscomputation indexescurrentSearchResults?.data[...]. The?.only guardscurrentSearchResults. WhencurrentSearchResultsexists butdatais undefined (for example while the snapshot is loading or being cleared, with selected reports still in state),undefined[...]throws. Every other access to this object in the file usescurrentSearchResults?.data?.[...], so this is the only unguarded one.App/src/hooks/useSearchBulkActions.ts
Lines 2921 to 2924 in 6502fec
What changes do you think we should make in order to solve the problem?
useSearchBulkActions.ts(around line 2924), change the lookup to optional chaining ondataas well:datais missing, the lookup gives undefined, soselectedMergeReportsends up shorter thanselectedReportsand the merge option is not shown. This is the correct behaviour.useSearchBulkActionstests, "does not crash when search results have no data": render the hook with selected reports andcurrentSearchResultsset to{search: {...}}with nodata. Expect no throw and no merge option..data[access without?.. I found only this one.What alternative solutions did you explore? (Optional)
currentSearchResults?.datais undefined. This is more code for the same result.searchResults(the last non-empty ref). This changes behaviour, and the code elsewhere deliberately uses the current results.MelvinBot commentedon Oct 12, 2026
Proposal
What is the root cause of that problem?
The merge-reports check in
useSearchBulkActionsreadscurrentSearchResults?.data[...]. The?.guards onlycurrentSearchResults, notdata. This is the only unguardeddataaccess in the file.datacan be missing at runtime. When a search starts for a new hash, its optimistic update creates the snapshot with onlysearchanderrors.datastays missing until the response arrives, or for good if the request fails. Report selection lives in a separate provider, so selected reports can still exist on that render. The lookup then reads fromundefinedand throws.TypeScript does not catch this because
SearchResults.datais typed as required.Code references
data?.[...], for example L753 and L2012. The hook also has an explicit!currentSearchResults?.datacheck at L662-L667.data: src/libs/actions/Search.ts:803-826. The failure data also leavesdataunset: L846-L859.currentSearchResults: src/components/Search/SearchResultsProvider.tsx:71datais typed as required: src/types/onyx/SearchResults.ts:309I didn't reproduce this in the app. The root cause comes from the stack trace and the code path.
What changes do you think we should make in order to solve the problem?
useSearchBulkActions.ts, add optional chaining ondatain theselectedMergeReportslookup, so it readscurrentSearchResults?.data?.[...].datais missing, every lookup returnsundefinedand gets filtered out. The lengths then don't match, so the hook hides the Merge option until results load. That is the right behavior.currentSearchResultswithsearchbut nodata, the hook should not throw and should not offer Merge.What alternative solutions did you explore? (Optional)
Make
dataoptional in theSearchResultstype so TypeScript flags unguarded accesses like this one. This prevents repeats, such as the possible sibling APP-C89. But it touches many call sites, so it fits a separate cleanup better than this fix.Note: The existing proposal from
haiquyworkdescribes the same root cause and fix.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/103775)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
mountiny commentedon Oct 12, 2026
(Vit's agent) @ikevin127, can you review the proposal from MelvinBot on this issue? Please tell us which of these fits:
janiya2 commentedon Oct 12, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
[Sentry: APP-KT0] Unhandled
TypeError: Cannot read properties of undefined (reading 'report_<reportID>')occurring inuseSearchBulkActionson the/searchpage.What is the root cause of that problem?
In
src/hooks/useSearchBulkActions.ts(around lines 2921-2925):The optional chaining operator
?.only guardscurrentSearchResults, but fails to guard.data.When search results are loading, being refreshed, or right after an optimistic clear where
currentSearchResultshas the structure{search: {...}}withoutdata,currentSearchResults?.dataevaluates toundefined. If reports are currently selected in state, indexing intoundefined[${ONYXKEYS.COLLECTION.REPORT}${reportID}]throws:Every other lookup in
useSearchBulkActions.ts(e.g. lines 753 and 2012) correctly usescurrentSearchResults?.data?.[...]. Line 2924 is the only unguarded access in the entire file.What changes do you think we should make in order to solve the problem?
In
src/hooks/useSearchBulkActions.ts(line 2924), add optional chaining to.data:const selectedMergeReports = areAllMatchingItemsSelected ? [] : selectedReports - .map(({reportID}) => currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]) + .map(({reportID}) => currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]) .filter((report) => !!report?.reportID && report.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);When
datais missing or undefined during loading states, every report lookup evaluates safely toundefinedand is discarded by.filter(...).selectedMergeReports.lengthwill not equalselectedReports.length, which gracefully suppresses the "Merge Reports" bulk action until search data has fully loaded.What alternative solutions did you explore? (Optional)
if (!currentSearchResults?.data) return []guard: Using optional chaining?.is cleaner, aligns with existing conventions in the file (lines 753, 2012), and minimizes boilerplate.Automated Test Scenarios
Add a unit test in
tests/unit/useSearchBulkActionsTest.ts:Contributor Details
jloa-dev commentedon Oct 12, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
In /search, performing or resetting bulk actions throws an unhandled \TypeError: Cannot read properties of undefined (reading 'report_')\ during render in \useSearchBulkActions.ts\ (Sentry: APP-KT0).
What is the root cause of that problem?
In \src/hooks/useSearchBulkActions.ts\ (line 2920 / line 2936):
\\ ypescript
const selectedMergeReports = areAllMatchingItemsSelected
? []
: selectedReports
.map(({reportID}) => currentSearchResults?.data[\\])
.filter((report) => !!report?.reportID && report.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
\
While all other calls across \useSearchBulkActions.ts\ safely access \currentSearchResults?.data?.[\\]\ (e.g. lines 752, 847, 1995, 2008, 2022, 2035, 2200, 2221), this mapping omits optional chaining on \currentSearchResults?.data.
When transitioning between search views or when the search query snapshot is reset while reports remain selected, \currentSearchResults?.data\ is \undefined, and indexing into it throws immediately during \useMemo\ render.
What changes do you think we should make in order to solve the problem?
In \src/hooks/useSearchBulkActions.ts:
Add optional chaining to \currentSearchResults?.data:
\\ ypescript
const selectedMergeReports = areAllMatchingItemsSelected
? []
: selectedReports
.map(({reportID}) => currentSearchResults?.data?.[\\])
.filter((report) => !!report?.reportID && report.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
\\
What alternative solutions did you explore? (Optional)
What specific scenarios should we cover in automated tests?
In \ ests/unit/useSearchBulkActionsTest.ts:
Contributor details
Your Expensify account email: jloa.dev@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~0163955a2ba22190aa
melvin-bot commentedon Oct 12, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
ikevin127 commentedon Oct 13, 2026
Sentry findings (APP-KT0):
selectedMergeReportslookup, now atsrc/hooks/useSearchBulkActions.ts:2936on main. It has 107 events from 98 users.9.4.73to9.5.6, not just9.5.5to9.5.7. About 95% are on/search, with a few on/search/r/:id,/search/view/:hashand/search/edit-multiple-transactions.currentSearchResults?.data[...]read atMoneyRequestView.tsx:232.useMergeTransactions.ts:59,85-88andSearchReportsMergeReports.tsx:226.Reliable repro (web, dev or staging, where
window.Onyxis available):type:expense-report) and tick one or more reports so the bulk actions button appears.With the fix applied, the same steps leave the page working and the Merge option simply doesn't show.
Detailed proposals review
Proposal 1 (MelvinBot), created
2026-10-12T14:37:38ZRoot cause: ✅ correct, verified on
main.MERGEinsrc/libs/actions/Search.tsonly writessearchanderrors. For a hash with no snapshot yet,datastaysundefineduntil the response lands, and stays that way if the request fails (failureDatadoesn't touchdata).SearchResultsProviderpasses the raw snapshot through ascurrentSearchResults.headerButtonsOptions/dropdownButtonsOptionsuseMemoonly checksselectedTransactionsKeys.lengthandhash, notdata. So a lingering selection runs straight into the unguarded lookup.clearSelectedTransactions()inSearch/index.tsxruns from an effect, so there's always at least one render where staleselectedReportssit next to a snapshot with nodata.SearchResults.datais typed as required, which is why TS never flagged it.Sentry matches: 107 events / 98 users, about 95% on
/search, every release from 9.4.73 through 9.5.6.Solution: ✅ correct.
currentSearchResults?.data?.[...]matches every other read in the hook (L755, L2001, L2014, ...). Thefilterthen drops theundefinedentries, the length check fails, and Merge stays hidden until results load. That's the right behavior.🟡 The scope is too narrow for the root cause. APP-C89 (345 events / 71 users) is a confirmed sibling with the same pattern at
src/components/ReportActionItem/MoneyRequestView.tsx:232:The same unguarded read also exists in
src/hooks/useMergeTransactions.ts:59,85-88andsrc/pages/Search/SearchReportsMergeReports.tsx:226. Each one is the same one-character fix with the same cause, and patching only one leaves the next crash already queued. I'd fold them into this PR.🟢 Test:
tests/unit/hooks/useSearchBulkActionsTest.tsalready exists, so the new case should go there instead of a new file. Given a selected report and acurrentSearchResultswithsearchbut nodata, assert that the hook doesn't throw and doesn't offerMERGE_REPORTS.🟢 Agree that making
dataoptional onSearchResultsis the real long-term guard, and that it belongs in a separate cleanup.@MelvinBot's proposal LGTM, with one addition: also fix the sibling
currentSearchResults?.data[...]reads listed above.On the options asked: 1, ready to go as it stands, no expert help needed.
🎀👀🎀 C+ reviewed
melvin-bot commentedon Oct 13, 2026
Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.
camesenin commentedon Oct 13, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
In search results view, performing bulk actions or transitioning search state while reports are selected crashes the app with
TypeError: Cannot read properties of undefined (reading 'report_<reportID>')becauseuseSearchBulkActions.tsunguardedly accessescurrentSearchResults.data[...]during transient states wheredatais undefined.What is the root cause of that problem?
In
src/hooks/useSearchBulkActions.ts(around line 2921-2925), theselectedMergeReportscalculation looks up reports from the active search results:The optional chaining
?.only guardscurrentSearchResults, but does not guard.data.During search transitions or when initiating a search query,
src/libs/actions/Search.ts:803-826optimistically writes a snapshot containingsearchanderrors, leavingdataundefined until the network request completes. Because selected reports are held in a separate state provider (SearchSelectionProvider), selected items can persist across renders whilecurrentSearchResults.datais undefined.Evaluating
currentSearchResults?.data[...]whendatais undefined immediately throws:TypeError: Cannot read properties of undefined (reading 'report_<reportID>').Every other reference in
useSearchBulkActions.tssafely usescurrentSearchResults?.data?.[...](e.g., lines 753 and 2012). Line 2921 was the sole unguarded access.What changes do you think we should make in order to solve the problem?
src/hooks/useSearchBulkActions.ts, add optional chaining to.data:datais undefined, the lookup resolves toundefinedand is discarded by the subsequent filter. This prevents any length match withselectedReportsand safely disables the Merge action until valid search data is available.tests/unit/useSearchBulkActionsTest.ts:currentSearchResultswheredataisundefined.canMergeremains false.What alternative solutions did you explore? (Optional)
SearchResults.datatype to optional (data?: ...): While desirable for long-term type safety, modifying the core Onyx type affects dozens of call sites across the codebase. Adding surgical optional chaining solves the runtime crash directly with zero regression risk.Contributor details
Your Expensify account email: camesenin@gmail.com
Upwork Profile link: https://www.upwork.com/freelancers/~0170d6b02e9440e604
melvin-bot commentedon Oct 13, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
mountiny commentedon Oct 13, 2026
@ikevin127 feel free to go ahead
ikevin127 commentedon Oct 13, 2026
@MelvinBot Implement this with the extended scope mentioned here in "Detailed proposals review", to open a draft PR.
MelvinBot commentedon Oct 13, 2026
🤖 Draft PR created: Guard search snapshot data reads that crash while a search is loading
Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.
feat: Add count and spend total selectors to the Spend footer already guards the main crash line in
useSearchBulkActions(APP-KT0), and it merged today. So this PR covers the extended scope:MoneyRequestView.tsx(APP-C89),useMergeTransactions.ts(getTransaction) andSearchReportsMergeReports.tsx.tests/unit/hooks/useSearchBulkActionsTest.ts.useMergeTransactions.tslines 85-88 were left unchanged. They're already inside anif (... currentSearchResults?.data)block, so they can't throw.Typecheck passes for the changed files. I couldn't run Jest, ESLint, formatting or the React Compiler check in this environment, so CI is the first run for those.
view run