Skip to content

[$175] [Sentry: APP-DRT] Web: category picker crashes when a category has no name #104745

Open

Description

@mountiny

Sentry

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

Impact (snapshot at filing)

  • Users (total since first seen): 22
  • Events: 154
  • Users (last 14d, all versions): 5
  • Users (last 14d, production 9.5.13 to 9.5.15): 2 (9.5.14: 1, 9.5.15: 1)
  • First seen: 2026-05-26
  • Last seen: 2026-10-22 (ongoing)
  • Platform: web (standalone)
  • App version(s): 9.5.4 to 9.5.15
  • Affected route(s) (parameterized): /search/view/:reportID
  • Mechanism: generic, level fatal, app_blocking: true

Stack trace (top frames, first-party only)

TypeError: Cannot read properties of undefined (reading 'split')
  src/libs/CategoryUtils.ts:234            processCategoryNameSegments   categoryName.split(CONST.PARENT_CHILD_SEPARATOR)
  src/libs/CategoryOptionListUtils.ts:281  sortCategories                processCategoryNameSegments(category.name)
  src/libs/CategoryOptionListUtils.ts:111  getCategoryListSections       sortCategories(categories, localeCompare)
  src/components/CategoryPicker/index.tsx:109  CategoryPicker render

Suspected cause

sortCategories expects every entry in the policy categories to have a string name. At least one entry has no name (for example an optimistic or partial category from Onyx), so processCategoryNameSegments calls split on undefined and the category picker crashes during render.

Reproduction

Unknown. See the Sentry events linked from the Sentry issue.

Related

  • Sibling Sentry IDs: none confirmed
  • Prior GH issues: none found
Upwork Automation - Do Not Edit

Activity

  1. self-assigned this
    on Oct 22, 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 22, 2026
  3. melvin-bot commented on Oct 22, 2026

    @melvin-bot

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

  4. changed the title [-][Sentry: APP-DRT] Web: category picker crashes when a category has no name[/-] [+][$175] [Sentry: APP-DRT] Web: category picker crashes when a category has no name[/+] on Oct 22, 2026
  5. melvin-bot commented on Oct 22, 2026

    @melvin-bot
  6. MelvinBot commented on Oct 22, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    CategoryPicker passes the policy categories from Onyx straight into sortCategories. That function calls processCategoryNameSegments(category.name) on every entry and never checks that name exists. One entry without a name crashes the picker during render.

    Nameless entries can appear because many category actions merge partial objects keyed by category name, with no name field (for example {pendingAction, pendingFields, errors}). If that key no longer exists when the merge lands, Onyx creates a new "ghost" entry with no name. This can happen when the server has already renamed or deleted the category.

    The crash path is confirmed in code. I couldn't confirm which action creates the ghost entry in production.

    Code references
    • Picker passes raw categories: CategoryPicker/index.tsx:107
    • Unguarded sort and split: CategoryOptionListUtils.ts:261-281. The later if (name) check at line 299 runs too late.
    • Likely sources of nameless entries, in Category.ts:
      • renamePolicyCategory success data merges {[newName]: {pendingAction, pendingFields}}: L944-L955
      • deleteWorkspaceCategories optimistic and failure data: L1357-L1363
      • Per-field setters (enabled, max amount, receipts required, and others) whose success and failure data omit name.
    • Not the cause: the Search snapshot (it doesn't hold policyCategories_), recently used categories, and the selected category. None of these reach sortCategories.
    • Git history shows no May 2026 code change that introduced this. The first-seen date likely reflects new data, not new code.

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

    1. In sortCategories, skip any category whose name is not a string before sorting. This protects both callers of sortCategories. Ghost entries have no useful data, so the list doesn't lose anything. A similar !!tagList.name filter already exists in src/libs/actions/Policy/Tag.ts.
    2. Add a unit test for getCategoryListSections with a category that has no name.

    What alternative solutions did you explore? (Optional)

    • Make processCategoryNameSegments return [] for a missing name. This is a wider safety net, but it hides bad data from other callers.
    • Root-cause hardening, as a separate change: include name in the per-key success and failure data in Category.ts. This doesn't cover a backend that sends partial categories, so the render guard is still needed.

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


    view run

  7. Abdallah-1551 commented on Oct 22, 2026

    @Abdallah-1551

    Proposal

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

    On web, opening the category picker (for example from /search/view/:reportID) throws TypeError: Cannot read properties of undefined (reading 'split'). The error is fatal and blocks the app. It happens when one entry in the policy categories Onyx collection has no name. CategoryPicker should not crash because of malformed or partial category data.

    What is the root cause of that problem?

    CategoryPicker passes the policyCategories Onyx value straight into getCategoryListSections, which calls sortCategories. sortCategories calls processCategoryNameSegments(category.name) for every entry, and that calls categoryName.split(CONST.PARENT_CHILD_SEPARATOR). Nothing checks that name is a non-empty string. The existing if (name) check later in getCategoryListSections runs after the crash point.

    Nameless entries ("ghost" categories) most likely come from Onyx merges keyed by category name that carry only metadata, such as {pendingAction, pendingFields, errors}. Examples are the success and failure data in renamePolicyCategory, deleteWorkspaceCategories, and the per-field setters in Category.ts. If the key no longer exists when the merge lands (for example, the server already renamed or deleted it), Onyx creates a new object without name. A backend response with partial categories would have the same effect. I couldn't confirm which of these produces the ghost entry in production, but the crash path is clear and the picker has to tolerate this data either way.

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

    1. Add a shared guard in src/libs/CategoryOptionListUtils.ts. Drop entries without a usable name before any name-based processing.
    function hasValidCategoryName(category: PolicyCategory | undefined): category is PolicyCategory {
        return typeof category?.name === 'string' && category.name.length > 0;
    }
    1. Apply the guard in sortCategories, which is the actual crash site and has more than one caller:
    function sortCategories(categories: Record<string, Category>, localeCompare: LocaleContextProps['localeCompare']): Category[] {
        const validCategories = Object.values(categories ?? {}).filter(hasValidCategoryName);
        // ...build the hierarchy and sort from `validCategories` instead of Object.values(categories)
    }
    1. Apply the guard at the top of getCategoryListSections so the other branches that read category.name are also safe. These are the search, selected-options, enabled-filter, and recently-used branches:
    const validCategories = Object.values(categories ?? {}).filter(hasValidCategoryName);
    const enabledCategories = validCategories.filter((category) => category.enabled);

    Then use validCategories or enabledCategories throughout the function instead of the raw categories.

    1. Keep processCategoryNameSegments strict. It continues to take a string. The data is cleaned at the boundary, so bad data isn't hidden from other callers.

    2. Follow-up hardening (optional, separate PR). In src/libs/actions/Policy/Category.ts, include name in the per-key optimistic, success, and failure data (rename, delete, and enabled, max amount, and receipts-required setters). This stops ghost entries from being created locally. The render guard is still needed for backend-originated partial data.

    3. Add unit tests in tests/unit/CategoryOptionListUtilsTest.ts:

      • getCategoryListSections with a categories map containing { ghost: { pendingAction: 'update' } } (no name) alongside valid ones. It should not throw, and the result should contain only the valid categories.
      • The same case with a non-empty searchValue, with selectedOptions, and with recentlyUsedCategories.
      • sortCategories with a nameless entry. It should not throw and should omit that entry.

    What alternative solutions did you explore? (Optional)

    • Make processCategoryNameSegments return [] for a missing name. This is a smaller change, but it only fixes one function. The nameless entry would still reach other code that reads category.name, and it could produce blank rows in the list. It also hides bad data from every other caller.
    • Filter only inside sortCategories. This fixes the reported stack trace but leaves other category.name accesses in getCategoryListSections unprotected. Guarding at both levels is cheap.
    • Filter with an Onyx selector in CategoryPicker. This only protects one component, and other callers of getCategoryListSections and sortCategories would remain exposed.
    • Fix only the producers in Category.ts. This doesn't cover data already stored in users' Onyx or partial backend data, so a render-side guard is still required.

    Testing

    1. Run the new unit tests and the existing CategoryOptionListUtilsTest, and confirm they pass.
    2. Manual reproduction:
      • Open a workspace report or expense that has categories, and use dev tools to merge a ghost entry into policyCategories_<policyID>, for example { "Ghost": { pendingAction: "update" } }.
      • Open the category picker, including through the Search report view /search/view/:reportID.
      • Before the fix, the picker crashes with the split error. After the fix, it renders only the valid categories.
      • Type in the search box and select a category to confirm the search and selection branches also work.
    3. Regression check: a policy with normal nested categories (Parent:Child) still renders the same hierarchy, order, and selected state.
    4. Confirm there are no crashes or console errors in the picker on web, and spot-check desktop and mobile.
  8. magomedta commented on Oct 22, 2026

    @magomedta

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~01777d047a06f0e470

    Proposal

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

    On Web (and other platforms), when opening the category picker (such as from /search/view/:reportID), the app crashes fatally with TypeError: Cannot read properties of undefined (reading 'split').

    What is the root cause of that problem?

    In src/libs/CategoryOptionListUtils.ts:

    1. sortCategories (called directly by getCategoryListSections:111) iterates over all entries of the categories dictionary and attempts to sort them:
      const sortedCategories = Object.values(categories).sort((a, b) => localeCompare(a.name, b.name));
    2. It then loops over sortedCategories at line 281:
      for (const category of sortedCategories) {
          const path = processCategoryNameSegments(category.name);
      and passes category.name directly into processCategoryNameSegments in src/libs/CategoryUtils.ts:234:
      function processCategoryNameSegments(categoryName: string) {
          return categoryName.split(CONST.PARENT_CHILD_SEPARATOR);
      }
    3. If Onyx contains any partial, optimistic, or deleted category entry lacking a name attribute (e.g. {pendingAction: 'update'} or {errors: ...} created by partial merges), category.name is undefined. Calling .split() on undefined causes a fatal runtime crash.

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

    In src/libs/CategoryOptionListUtils.ts:

    1. Filter out entries with invalid or missing name at the entry point of sortCategories.
    2. Also ensure getCategoryListSections uses guarded category entries so secondary lookups (enabledCategories, search, selectedOptions) never encounter nameless entries.
    --- a/src/libs/CategoryOptionListUtils.ts
    +++ b/src/libs/CategoryOptionListUtils.ts
    @@ -260,7 +260,10 @@ function buildHierarchy(categories: Category[]) {
     }
     
     function sortCategories(categories: Record<string, Category>, localeCompare: LocaleContextProps['localeCompare']): Category[] {
    -    const sortedCategories = Object.values(categories).sort((a, b) => localeCompare(a.name, b.name));
    +    const validCategories = Object.values(categories ?? {}).filter(
    +        (category): category is Category & {name: string} => typeof category?.name === 'string' && category.name.trim().length > 0,
    +    );
    +    const sortedCategories = validCategories.sort((a, b) => localeCompare(a.name, b.name));
     
         // An object that respects nesting of categories. Also, can contain only uniq categories.
         const hierarchy: Hierarchy = {};

    Key Advantages:

    1. Direct crash site protection: Defends both localeCompare(a.name, b.name) (which also throws or fails when name is undefined) and processCategoryNameSegments(category.name).
    2. Defensive data boundary: Prevents ghost/malformed Onyx records from corrupting the category hierarchy structure.
    3. Zero side-effects: Valid categories continue sorting and nesting as expected (Parent: Child).

    What alternative solutions did you explore? (Optional)

    • Adding a fallback in processCategoryNameSegments(categoryName: string = ''):
      Rejected. While this prevents the .split() crash inside that single helper, it allows invalid empty items into the hierarchy, which can lead to empty list items in the UI or undefined keys in localeCompare. Sanitizing in sortCategories is cleaner and safer.

    What automated tests can you add to prevent this from re-occurring?

    In tests/unit/CategoryOptionListUtilsTest.ts:

    • Add a test case for sortCategories passing a categories dictionary containing a nameless entry:
      it('should ignore categories without a valid name during sorting and hierarchy creation', () => {
          const mockCategories = {
              ValidCategory: {name: 'ValidCategory', enabled: true},
              GhostCategory: {pendingAction: 'update', enabled: false} as unknown as Category,
          };
          const result = sortCategories(mockCategories, (a, b) => a.localeCompare(b));
          expect(result).toHaveLength(1);
          expect(result[0].name).toBe('ValidCategory');
      });
  9. melvin-bot commented on Oct 22, 2026

    @melvin-bot

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

  10. taieddie commented on Oct 22, 2026

    @taieddie

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~01af851c9a7f771775

    Proposal

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

    The category picker crashes when rendering policy categories if any category record lacks a valid string name.

    What is the root cause of that problem?

    In src/libs/CategoryOptionListUtils.ts, sortCategories loops over categories and calls processCategoryNameSegments(category.name). In src/libs/CategoryUtils.ts line 234, processCategoryNameSegments expects categoryName to be a string and directly invokes categoryName.split(CONST.PARENT_CHILD_SEPARATOR) without verifying if categoryName is defined. When Onyx contains a partial or malformed category entry where category.name is undefined, split throws an unhandled TypeError: Cannot read properties of undefined (reading 'split').

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

    1. In src/libs/CategoryUtils.ts:

      • In processCategoryNameSegments, guard against missing or non-string inputs by defaulting categoryName or returning an empty array immediately when !categoryName or typeof categoryName !== 'string'.
    2. In src/libs/CategoryOptionListUtils.ts:

      • In getCategoryListSections, sanitize the category list input before passing it to sortCategories by filtering out entries without a valid name (e.g., Boolean(category?.name)).
      • Alternatively or companion to the above, inside sortCategories, verify category?.name before calling processCategoryNameSegments(category.name) so sorting comparator logic handles nameless records defensively.

    What alternative solutions did you explore? (Optional)

    I considered filtering nameless categories exclusively within sortCategories. However, guarding only inside sortCategories leaves other consumers of raw Onyx policyCategories (such as direct callers of processCategoryNameSegments across CategoryUtils.ts) vulnerable to the exact same crash when encountering unexpected ghost entries. Addressing the issue by sanitizing entries at the collection boundary in getCategoryListSections while also adding a defensive early return in processCategoryNameSegments provides complete defense without breaking backwards compatibility.

  11. melvin-bot commented on Oct 22, 2026

    @melvin-bot

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

  12. HamzaKhanBUIC commented on Oct 22, 2026

    @HamzaKhanBUIC

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: Will provide shortly (currently encountering temporary rate limit on Upwork)

    Proposal

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

    On Web, opening the Category Picker can trigger a fatal crash: TypeError: Cannot read properties of undefined (reading 'split') in processCategoryNameSegments when a policy category entry in Onyx does not have a valid name string (e.g. corrupted, pending delete, or empty optimistic record).

    What is the root cause of that problem?

    1. In src/libs/CategoryOptionListUtils.ts:259-281, sortCategories() converts categories to an array via Object.values(categories) and assumes every entry has a valid string name:
      const sortedCategories = Object.values(categories).sort((a, b) => localeCompare(a.name, b.name));
      for (const category of sortedCategories) {
          const path = processCategoryNameSegments(category.name);
    2. If any category record in policyCategories_<policyID> lacks a name property (or name is undefined), sortCategories passes undefined to localeCompare and processCategoryNameSegments.
    3. In src/libs/CategoryUtils.ts:234, processCategoryNameSegments unconditionally calls:
      const parts = categoryName.split(CONST.PARENT_CHILD_SEPARATOR);
      which throws TypeError: Cannot read properties of undefined (reading 'split').

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

    1. In src/libs/CategoryOptionListUtils.ts (sortCategories):
      Filter for valid category records with non-empty string names before sorting and building the hierarchy tree:

      --- a/src/libs/CategoryOptionListUtils.ts
      +++ b/src/libs/CategoryOptionListUtils.ts
      @@ -258,7 +258,8 @@ function getCategoryOptionTree(
       function sortCategories(categories: Record<string, Category>, localeCompare: LocaleContextProps['localeCompare']): Category[] {
           // Sorts categories alphabetically by name.
      -    const sortedCategories = Object.values(categories).sort((a, b) => localeCompare(a.name, b.name));
      +    const validCategories = Object.values(categories ?? {}).filter((category): category is Category & {name: string} => typeof category?.name === 'string' && category.name.trim() !== '');
      +    const sortedCategories = validCategories.sort((a, b) => localeCompare(a.name, b.name));
      
           // An object that respects nesting of categories. Also, can contain only uniq categories.
           const hierarchy: Hierarchy = {};
    2. In src/libs/CategoryUtils.ts (processCategoryNameSegments):
      Add a defensive falsy guard so the function safely returns an empty array if passed undefined, null, or an empty string:

      --- a/src/libs/CategoryUtils.ts
      +++ b/src/libs/CategoryUtils.ts
      @@ -232,6 +232,9 @@ function getCategoryName(category: OnyxEntry<Category>): string {
        */
      -function processCategoryNameSegments(categoryName: string): string[] {
      +function processCategoryNameSegments(categoryName: string = ''): string[] {
      +    if (!categoryName) {
      +        return [];
      +    }
           const parts = categoryName.split(CONST.PARENT_CHILD_SEPARATOR);
           const result: string[] = [];

    What alternative solutions did you explore? (Optional)

    Only adding the guard in processCategoryNameSegments was explored, but without filtering in sortCategories, localeCompare(a.name, b.name) would still receive undefined, which can cause sorting inconsistencies. Filtering in sortCategories guarantees clean data throughout the hierarchy builder.

    Testing

    1. Unit Tests:
      • In tests/unit/CategoryOptionListUtilsTest.ts, test sortCategories with a dictionary containing a category with name: undefined or missing name. Assert that it does not throw and returns sorted valid categories.
      • In tests/unit/CategoryUtilsTest.ts, test processCategoryNameSegments('') and processCategoryNameSegments(undefined as unknown as string) to confirm it returns [] without error.
    2. Manual Testing:
      • Open a workspace, navigate to Categories, and open Category Picker. Verify categories render and sort properly.
  13. melvin-bot commented on Oct 22, 2026

    @melvin-bot

    ⚠️ Invalid email. Please make sure to create an Expensify account with this email first here.

  14. HamzaKhanBUIC commented on Oct 22, 2026

    @HamzaKhanBUIC

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/hamzakhanbuic

  15. melvin-bot commented on Oct 22, 2026

    @melvin-bot

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

  16. HamzaKhanBUIC commented on Oct 22, 2026

    @HamzaKhanBUIC

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~015055563b6090af39

  17. melvin-bot commented on Oct 22, 2026

    @melvin-bot

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

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 contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions