Skip to content

[$175] [Sentry: APP-J60] Cannot read 'submitsTo' of null in getSuggestedSearchesVisibility #103774

Open

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-J60

Impact (snapshot at filing, 2026-10-12)

  • Users (total since first seen): 1997
  • Users (last 30d, releases 9.5.5 to 9.5.7 only): 859
  • Events (30d window): 1310
  • First seen: 2026-07-22
  • Last seen: ongoing
  • Platform: iOS hybrid app (also seen on web)
  • App version(s): 9.5.5
  • Affected route(s): SearchMoneyRequestReport / Search
  • Mechanism: generic (unhandled TypeError during render)

Stack trace (top frames, first-party only)

SearchQueryProvider
getSuggestedSearchesVisibility
_loop (Array.some)

Suspected cause

A null policy or workflow object reaches getSuggestedSearchesVisibility while SearchQueryProvider renders. The code reads submitsTo on it without a null check.

Reproduction

Unknown. See the events linked from the Sentry issue.

Related

  • Sibling Sentry IDs: none
  • Prior GH issues: none found
Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @suneox

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 - @suneox (External)

  4. changed the title [-][Sentry: APP-J60] Cannot read 'submitsTo' of null in getSuggestedSearchesVisibility[/-] [+][$175] [Sentry: APP-J60] Cannot read 'submitsTo' of null in getSuggestedSearchesVisibility[/+] 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 getSuggestedSearchesVisibility, the isSubmittedTo check iterates Object.values(policy.employeeList ?? {}) and reads employee.submitsTo / employee.forwardsTo without checking the entry. employeeList is a map of email to employee, and entries can be null (for example a member removed by an optimistic or server update that sets the key to null). A null entry then throws inside Array.some. The policy itself is already null-guarded just above, so the null is the employee entry, not the policy.

    const isSubmittedTo =
    !!currentUserEmail && Object.values(policy.employeeList ?? {}).some((employee) => employee.submitsTo === currentUserEmail || employee.forwardsTo === currentUserEmail);

    SearchQueryProvider calls this on every render with all policies, so the error crashes the whole Search page.
    const {shouldShowExpensifyCard} = getSuggestedSearchesVisibility(email, {}, policies, undefined);

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

    • In getSuggestedSearchesVisibility (src/libs/SearchUIUtils.ts), make the isSubmittedTo callback null-safe:
    Object.values(policy.employeeList ?? {}).some((employee) => employee?.submitsTo === currentUserEmail || employee?.forwardsTo === currentUserEmail)
    • Check the other employeeList loops in this function (and isPolicyEligibleForTopSpenders) for the same unguarded access and apply the same optional chaining.
    • Add a unit test in tests/unit/Search/SearchUIUtilsTest.ts, e.g. "getSuggestedSearchesVisibility does not throw when employeeList contains a null entry". It passes a policy with employeeList: {'a@b.com': null} and expects no throw and a valid visibility result. The test should fail on current main.

    What alternative solutions did you explore? (Optional)

    • Strip null employees when policies are read from Onyx: it touches many consumers and is a larger change than a local null check.
  7. mountiny commented on Oct 12, 2026

    @mountiny
    ContributorAuthor

    (Vit's agent) @suneox, can you review the proposal on this issue? There is no proposal from Melvin here yet, only the one above from a contributor. 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.
  8. MelvinBot commented on Oct 12, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    policy.employeeList sometimes holds a null entry. getSuggestedSearchesVisibility reads employee.submitsTo on every entry without a null check, so it throws. The policy itself is already guarded at line 733. The sibling helper isPolicyApprover already uses employee?..

    The impact is large because SearchQueryProvider calls this function on every render. That provider wraps the whole signed-in navigator (AuthScreens.tsx:193), so one bad entry can break every screen, not only Search. Scope Top Spenders suggested search to eligible workspace policyIDs added this call (merged 2026-07-20, two days before Sentry first saw the error). Persist filters across view in Search brought it back after a revert.

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

    1. In getSuggestedSearchesVisibility, use employee?.submitsTo and employee?.forwardsTo, matching isPolicyApprover.
    2. Add the same employee?. guard to three other places that read entries the same unguarded way:
    3. Add a unit test that passes employeeList: {'a@b.com': null} to getSuggestedSearchesVisibility and checks that it doesn't throw.

    The existing proposal above covers steps 1 and 3.

    What alternative solutions did you explore? (Optional)

    • Type PolicyEmployeeList as Record<string, PolicyEmployee | null | undefined>. TypeScript would then flag every unguarded read. This is a wider change, so it fits better as a follow-up.
    • Strip null entries when policies load from Onyx. This touches many consumers, and the exact source of the null isn't confirmed (see below).
    Investigation details (where the null comes from is unconfirmed)
    • The app's own member-removal writes set employeeList: {[email]: null}, for example in Member.ts:414 and Member.ts:1233. They use MERGE, which normally removes nested nulls.
    • One possible gap, based on reading the code without reproducing it: when Onyx.update batches several policy keys and an earlier operation sets employeeList: null, the later employee list replaces the object without removing nulls. App code writes employeeList: null only on rare failure paths (Policy.ts:3093, Policy.ts:3746). The backend may also send this pattern.
    • Bump Onyx to 3.0.89 (re-land of #93436) merged 2026-07-21, close to the first-seen date. It may have made these nulls appear more often.
    • PolicyEmployeeList is typed as Record<string, PolicyEmployee>, so TypeScript never flags these reads.
    • I couldn't pull Sentry event details for this run, so I couldn't confirm what users did just before the crash.
    • No related open issues found.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/103774) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  9. suneox commented on Oct 12, 2026

    @suneox
    Contributor

    @haiquywork Please follow our contributor guideline do not post proposals on issues without the Help Wanted label.

  10. suneox commented on Oct 12, 2026

    @suneox
    Contributor

    This doesn't need an expert contributor, it isn't related to native code, navigation, FlashList. It's a missing null check on the workspace member list

    Possible causes in production:

    • From App code alone (rare): an admin is offline, creates a new workspace and removes a member from it. When the app reconnects, the workspace creation fails while the member removal succeeds, and both results are applied in the same batch.
    • Main suspect (not confirmed yet): data sent by the server. In Sentry, all events come from admins and most crashes happen right after the app reconnects and receives data for several workspaces at once.

    So Melvin proposal is straightforward to fix the issue.

    @MelvinBot let implement this

    🎀 👀 🎀 C+ reviewed

  11. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.

  12. MelvinBot commented on Oct 12, 2026

    @MelvinBot
    Contributor

    🤖 Draft PR created: Guard against null workspace member entries in employeeList
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    The PR skips null employeeList entries in getSuggestedSearchesVisibility and in the other loops from the proposal. It also guards two more loops in DynamicWorkspaceWorkflowsApprovalsExpensesFromPage.tsx that read entries the same way. The new unit test fails on main with a TypeError and passes with the fix.


    view run

  13. camesenin commented on Oct 13, 2026

    @camesenin

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When navigating or loading search suggestions, getSuggestedSearchesVisibility crashes with TypeError: Cannot read properties of null (reading 'submitsTo') when inspecting policy.employeeList containing null member entries (e.g., following optimistic member removal or deletion).

    What is the root cause of that problem?

    In src/libs/SearchUIUtils.ts, getSuggestedSearchesVisibility determines whether the current user is an approver by inspecting policy.employeeList:

    const isSubmittedTo = Object.values(policy.employeeList ?? {}).some(
        (employee) => employee.submitsTo === currentUserEmail || employee.forwardsTo === currentUserEmail,
    );

    While policy itself is guarded against null/undefined, entries in policy.employeeList can be null at runtime (for instance, when workspace members are removed or updated via optimistic Onyx actions such as Member.ts which set { [email]: null } during deletion).

    When Object.values iterates over employeeList, it encounters a null entry. Directly accessing .submitsTo on null throws:
    TypeError: Cannot read properties of null (reading 'submitsTo').

    Because SearchQueryProvider.tsx:66 executes getSuggestedSearchesVisibility across all workspace policies on every render, this exception unhandled at the top level causes SearchQueryProvider to crash, tearing down the authenticated screen tree.

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

    1. In src/libs/SearchUIUtils.ts inside getSuggestedSearchesVisibility, add optional chaining when accessing employee:
    const isSubmittedTo = Object.values(policy.employeeList ?? {}).some(
        (employee) => employee?.submitsTo === currentUserEmail || employee?.forwardsTo === currentUserEmail,
    );
    1. Apply the same defensive optional chaining employee?. across sibling unguarded accesses:
      • src/libs/WorkflowUtils.ts:827
      • src/libs/WorkflowUtils.ts:1847-1850
      • src/pages/workspace/workflows/approvals/DynamicWorkspaceWorkflowsApprovalsExpensesFromPage.tsx:124
        This aligns with the pattern already used in src/libs/PolicyUtils/permissions.ts (isPolicyApprover).
    2. Add a unit test in tests/unit/Search/SearchUIUtilsTest.ts:
      • Pass a mock policy with an employeeList containing a null entry (e.g. { 'removed@user.com': null }).
      • Verify that getSuggestedSearchesVisibility executes successfully without throwing and returns correct visibility booleans.

    What alternative solutions did you explore? (Optional)

    • Sanitizing/stripping null entries in Onyx policy subscribers: Rejected because it adds overhead across all policy subscribers and touches global state rather than fixing the local property dereferencing.

    Contributor details
    Your Expensify account email: camesenin@gmail.com
    Upwork Profile link: https://www.upwork.com/freelancers/~0170d6b02e9440e604

  14. melvin-bot commented on Oct 13, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  15. mountiny commented on Oct 13, 2026

    @mountiny
    ContributorAuthor

    @suneox feel free to go ahead

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.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorReviewingHas a PR in review

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions