Skip to content

[Due for payment 2026-10-26] [$175] [Sentry: APP-KT0] Search bulk actions crash reading undefined search results data #103775

Open

Description

@mountiny

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
Issue OwnerCurrent Issue Owner: @ikevin127

Activity

  1. self-assigned this
    on Oct 12, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 12, 2026
  3. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @ikevin127 (External)

  4. changed the title [-][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[/+] on Oct 12, 2026
  5. melvin-bot commented on Oct 12, 2026

    @melvin-bot
  6. haiquywork commented on Oct 12, 2026

    @haiquywork

    Proposal

    What is the root cause of that problem?

    In useSearchBulkActions, the selectedMergeReports computation indexes currentSearchResults?.data[...]. The ?. only guards currentSearchResults. When currentSearchResults exists but data is 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 uses currentSearchResults?.data?.[...], so this is the only unguarded one.

    const selectedMergeReports = areAllMatchingItemsSelected
    ? []
    : selectedReports
    .map(({reportID}) => currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`])

    What changes do you think we should make in order to solve the problem?

    • In useSearchBulkActions.ts (around line 2924), change the lookup to optional chaining on data as well:
    .map(({reportID}) => currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`])
    • When data is missing, the lookup gives undefined, so selectedMergeReports ends up shorter than selectedReports and the merge option is not shown. This is the correct behaviour.
    • Add a unit test in the useSearchBulkActions tests, "does not crash when search results have no data": render the hook with selected reports and currentSearchResults set to {search: {...}} with no data. Expect no throw and no merge option.
    • Grep the file for any other .data[ access without ?.. I found only this one.

    What alternative solutions did you explore? (Optional)

    • Skip the merge computation when currentSearchResults?.data is undefined. This is more code for the same result.
    • Fall back to searchResults (the last non-empty ref). This changes behaviour, and the code elsewhere deliberately uses the current results.
  7. MelvinBot commented on Oct 12, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The merge-reports check in useSearchBulkActions reads currentSearchResults?.data[...]. The ?. guards only currentSearchResults, not data. This is the only unguarded data access in the file.

    data can be missing at runtime. When a search starts for a new hash, its optimistic update creates the snapshot with only search and errors. data stays 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 from undefined and throws.

    TypeScript does not catch this because SearchResults.data is typed as required.

    Code references

    I 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?

    1. In useSearchBulkActions.ts, add optional chaining on data in the selectedMergeReports lookup, so it reads currentSearchResults?.data?.[...].
    2. When data is missing, every lookup returns undefined and gets filtered out. The lengths then don't match, so the hook hides the Merge option until results load. That is the right behavior.
    3. Add a unit test. Given selected reports and currentSearchResults with search but no data, the hook should not throw and should not offer Merge.

    What alternative solutions did you explore? (Optional)

    Make data optional in the SearchResults type 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 haiquywork describes 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

  8. mountiny commented on Oct 12, 2026

    @mountiny
    ContributorAuthor

    (Vit's agent) @ikevin127, can you review the proposal from MelvinBot on this issue? Please tell us which of these fits:

    1. This is ready to open for contributors as it stands.
    2. We should ask for expert contributor help, because it needs deeper native, navigation or FlashList knowledge.
    3. The proposal needs changes first.
  9. janiya2 commented on Oct 12, 2026

    @janiya2

    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 in useSearchBulkActions on the /search page.


    What is the root cause of that problem?

    In src/hooks/useSearchBulkActions.ts (around lines 2921-2925):

    const selectedMergeReports = areAllMatchingItemsSelected
        ? []
        : selectedReports
              .map(({reportID}) => currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`])
              .filter((report) => !!report?.reportID && report.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);

    The optional chaining operator ?. only guards currentSearchResults, but fails to guard .data.

    When search results are loading, being refreshed, or right after an optimistic clear where currentSearchResults has the structure {search: {...}} without data, currentSearchResults?.data evaluates to undefined. If reports are currently selected in state, indexing into undefined[${ONYXKEYS.COLLECTION.REPORT}${reportID}] throws:

    TypeError: Cannot read properties of undefined (reading 'report_...')
    

    Every other lookup in useSearchBulkActions.ts (e.g. lines 753 and 2012) correctly uses currentSearchResults?.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 data is missing or undefined during loading states, every report lookup evaluates safely to undefined and is discarded by .filter(...). selectedMergeReports.length will not equal selectedReports.length, which gracefully suppresses the "Merge Reports" bulk action until search data has fully loaded.


    What alternative solutions did you explore? (Optional)

    • Adding an explicit 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:

    describe('useSearchBulkActions', () => {
        it('does not throw and omits merge bulk action when currentSearchResults.data is undefined', () => {
            const {result} = renderHook(() =>
                useSearchBulkActions({
                    currentSearchResults: {search: {isLoading: true}} as any,
                    selectedReports: [{reportID: '12345'} as any],
                }),
            );
    
            expect(() => result.current.bulkActions).not.toThrow();
            const hasMergeOption = result.current.bulkActions?.some(
                (action) => action.value === CONST.SEARCH.BULK_ACTION_TYPES.MERGE_REPORTS,
            );
            expect(hasMergeOption).toBe(false);
        });
    
        it('includes merge bulk action when currentSearchResults.data contains all selected reports', () => {
            // Verify merge bulk action is offered when data is populated and merge conditions pass
        });
    });

    Contributor Details

  10. jloa-dev commented on Oct 12, 2026

    @jloa-dev

    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)

    • Guarding \currentSearchResults?.data\ at the top of the option builder block: while functional, using ?.\ directly preserves idiomatic consistency with all other report snapshot readers in this hook.

    What specific scenarios should we cover in automated tests?

    In \ ests/unit/useSearchBulkActionsTest.ts:

    1. Verify that building search bulk actions with selected reports when \currentSearchResults.data\ is \undefined\ or empty does not throw and returns empty merge options.

    Contributor details
    Your Expensify account email: jloa.dev@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0163955a2ba22190aa

  11. 8 remaining items

  12. changed the title [-][$175] [Sentry: APP-KT0] Search bulk actions crash reading undefined search results data[/-] [+][Due for payment 2026-10-26] [$175] [Sentry: APP-KT0] Search bulk actions crash reading undefined search results data[/+] on Oct 19, 2026
  13. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    @ikevin127

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.13-6 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-10-26. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  14. ikevin127 commented on Oct 19, 2026

    @ikevin127
    Contributor

    BugZero Checklist

    • [Contributor] The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to comment: https://github.com/Expensify/App/pull/95658/changes#r4318638431

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion: N/A

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    • [BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.

      Link to issue:

    Regression Test Proposal

    Platforms: wide layout only (desktop web, desktop app, or tablet in landscape). N/A on native phones, since leaving selection mode there clears the selection.

    1. Log in with an account that has at least two expense reports.
    2. Go to Spend > Reports.
    3. Tick the checkbox on one or more reports so the bulk actions button appears.
    4. Without clearing the selection, click Filters.
    5. Click Date and select a preset you haven't used yet in this session (e.g. This month).
    6. Verify that the search list shows a brief loading state, then the new results show, and the app doesn't crash.

    Do we agree 👍 or 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorMonthlyKSv2WeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions