Skip to content

[Due for payment 2026-11-05] [$175] [Sentry: APP-MHA] HybridApp iOS: FlashList onScroll throws "LayoutManager is not initialized" #104502

Open

Description

@mountiny

Sentry

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

Impact (snapshot at filing)

  • Users (total since first seen): 37
  • Events: 106
  • Users (last 14d, all versions): 21
  • Users (last 14d, production 9.5.11 to 9.5.13): 7 (54 events)
  • First seen: 2026-09-30 (about 3 weeks ago)
  • Last seen: 2026-10-20 (ongoing)
  • Platform: iOS HybridApp (Hermes, Fabric)
  • App version(s): 9.4.94 to 9.5.14. Most users are on 9.5.13 and 9.5.5.
  • Affected route(s) (parameterized): native screen; the sample event was on Workspaces_List
  • Mechanism: onerror, level fatal

Stack trace (top frames, first-party only)

The stack has no first-party frame. These are the FlashList frames:

@shopify/flash-list/dist/recyclerview/RecyclerView.js:269        onScroll handler -> recyclerViewManager.recordInteraction()
@shopify/flash-list/dist/recyclerview/RecyclerViewManager.js:218 recordInteraction -> itemViewabilityManager.recordInteraction()
getWindowSize -> Error: LayoutManager is not initialized, window size is unavailable

Bundle frames in order: dispatchEvent -> batchedUpdates$1 -> executeDispatch -> recordInteraction -> getWindowSize.

Suspected cause

A native scroll event reaches a FlashList before its layout manager exists, or after it is torn down. FlashList's own onScroll calls recordInteraction(), which reads the window size and throws. The App already catches this error for the synthetic scroll path in src/components/MoneyRequestReportView/ExternalScrollFlashListTable.tsx (around line 114), but native scroll events go through FlashList's onScroll with no guard.

Reproduction

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

Related

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @linhvovan29546

Activity

  1. self-assigned this
    on Oct 20, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 20, 2026
  3. melvin-bot commented on Oct 20, 2026

    @melvin-bot

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

  4. changed the title [-][Sentry: APP-MHA] HybridApp iOS: FlashList onScroll throws "LayoutManager is not initialized"[/-] [+][$175] [Sentry: APP-MHA] HybridApp iOS: FlashList onScroll throws "LayoutManager is not initialized"[/+] on Oct 20, 2026
  5. melvin-bot commented on Oct 20, 2026

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

    @haiquywork

    Proposal

    What is the root cause of that problem?

    The throw is inside FlashList 2.3.2. Its onScroll calls recyclerViewManager.recordInteraction() once isInitialScrollComplete is true, and that calls ViewabilityManager.updateViewableItems(). updateViewableItems() calls getWindowSize(), which throws when layoutManager is undefined. The layoutManager is undefined before the first measure and is reset to undefined when it is marked dirty (RecyclerViewManager.js ~L157). FlashList's own computeItemViewability() sits behind shouldListenToVisibleIndices, but recordInteraction() has no hasLayout() check. A native scroll event that lands in that window therefore throws from the React event handler, and onerror reports it as fatal.
    In our code src/components/Table/TableBody.tsx (the WorkspaceListTable list) and src/components/FlashList/index.tsx pass the FlashList straight through. Only the synthetic-scroll path in ExternalScrollFlashListTable.tsx has a try/catch. I could not reproduce this on a device. I confirmed the FlashList code path by reading node_modules.

    useEffect(() => {
    if (!store) {
    return;
    }
    const emit = () => {
    // Offset into the nested list's own coordinate space (FlashList subtracts its measured firstItemOffset —
    // the header height — internally). Only `contentOffset.y` is read for a vertical list, so nothing else is
    // populated; windowing comes from the `overrideWindowSize` prop, not this event.
    const y = Math.max(0, store.getOffset() - offsetTop);
    try {
    // Synthesizing a native scroll event requires an assertion: NativeSyntheticEvent's target/currentTarget
    // are RN HostInstances that can't be constructed in JS. FlashList's vertical handler reads only
    // contentOffset.y.
    // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
    onScroll?.({nativeEvent: {contentOffset: {x: 0, y}}} as NativeSyntheticEvent<NativeScrollEvent>);
    } catch {
    // During back-navigation teardown FlashList can be driven after its layout manager is gone, which
    // throws ("LayoutManager is not initialized"). This call only feeds a synthetic scroll frame to
    // window the nested list, so a dropped frame here is harmless — swallow it rather than crash.
    }
    };
    // Seed the initial window, then track subsequent parent scrolls. Re-subscribes only when the store, the
    // measured offset, or FlashList's scroll handler change — none of which happen per scroll frame.
    emit();

    <FlashList<TableData>
    ref={(instance) => {
    listRef.current = instance;
    measureScrollbarRef(instance);
    }}
    data={listData}
    style={[styles.flex1, styles.mnh0]}
    maintainVisibleContentPosition={{disabled: true}}
    ListHeaderComponent={listHeaderContent}
    ListHeaderComponentStyle={[
    ListHeaderComponentStyle,
    // An unmeasured page header leaves nothing to offset the stack by, and 0 would pin it instead.
    pageHeaderHeight > 0 && shouldRenderColumnHeaderInListHeader && getColumnScrollListHeaderStyle(pageHeaderHeight),
    ]}
    ListEmptyComponent={shouldRenderEmptyStateInList ? emptyStateContent : ListEmptyComponent}
    ListEmptyComponentStyle={[ListEmptyComponentStyle, shouldRenderEmptyStateInList && styles.flexGrow1, shouldRenderEmptyStateInList && styles.justifyContentCenter]}
    ListFooterComponent={ListFooterComponent}
    ListFooterComponentStyle={shouldRenderEmptyStateInList ? emptyStateFooterStyle : ListFooterComponentStyle}
    onLoad={handleLoad}
    onChangeStickyIndex={handleChangeStickyIndex}
    stickyHeaderIndices={hasRows && canRenderStickyHeader ? adjustedStickyHeaderIndices : undefined}
    contentContainerStyle={[
    listContentContainerStyle,
    tableBodyContentContainerStyle,
    contentContainerStyle,
    // Absolutely positioned rows don't widen the scroller's content, so hold the scroll extent open.
    isColumnScrollEnabled && StyleUtils.getMinimumWidth(scrollWidth),
    shouldRenderEmptyStateInList && styles.flexGrow1,
    shouldUseNarrowTableLayout &&
    typeof contentMinHeight === 'number' &&
    typeof tableBodyBottomPadding === 'number' && {
    minHeight: contentMinHeight + tableBodyBottomPadding,
    },
    ]}
    keyboardShouldPersistTaps="handled"
    onScrollBeginDrag={handleScrollBeginDrag}
    renderItem={renderListItem}
    keyExtractor={keyExtractorForList}
    getItemType={getItemTypeForList}
    onEndReached={hasRows ? onEndReached : undefined}
    onStartReached={hasRows ? onStartReached : undefined}
    initialScrollIndex={initialScrollIndexForList}
    onViewableItemsChanged={hasRows && onViewableItemsChanged ? handleViewableItemsChanged : undefined}
    overrideItemLayout={overrideItemLayout ? overrideItemLayoutForList : undefined}
    viewabilityConfigCallbackPairs={hasRows ? viewabilityConfigCallbackPairsForList : undefined}
    onScroll={(event) => {
    trackScrollOffset(event);
    onScroll?.(event);
    }}
    {...restListProps}
    scrollEnabled={scrollEnabled}

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

    • The cleanest fix is to guard recordInteraction in the library. Patch @shopify/flash-list with patch-package, which the repo already uses, so recordInteraction() or updateViewableItems() returns early when !hasLayout(). computeItemViewability already behaves this way.
    • This fixes the write site, so every FlashList in the app is covered, including the ones that don't wrap src/components/FlashList. I'd also send the same guard upstream to Shopify.
    • If a patch isn't wanted, the fallback is to stop un-guarded scroll events from reaching the list. This does not stop the throw from RecyclerView.onScroll itself, which is why I prefer the patch.
    • Add a patch note in patches/@shopify/flash-list/details.md, as the repo requires for patches.
    • Add a unit test next to the existing FlashList tests. Given a RecyclerViewManager with no layout, When recordInteraction() is called, Then it does not throw. It should fail on the unpatched code.

    What alternative solutions did you explore? (Optional)

    • Wrap onScroll in try/catch in TableBody.tsx. This doesn't work, because FlashList throws before it calls the user's onScroll, and it would only cover one list.
    • Catch the error globally. This hides real errors.
  7. jloa-dev commented on Oct 21, 2026

    @jloa-dev

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

    Proposal

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

    On iOS HybridApp (Hermes / Fabric), scrolling a FlashList (e.g. on native screen Workspaces_List or during screen transitions) crashes the application with a fatal error:
    Error: LayoutManager is not initialized, window size is unavailable
    thrown from @shopify/flash-list/dist/recyclerview/RecyclerViewManager.js -> getWindowSize(), called via recyclerViewManager.recordInteraction() from RecyclerView.js:269's onScroll handler.

    What is the root cause of that problem?

    1. On iOS Fabric, native scroll momentum events (onScroll) can be dispatched asynchronously from the native UI thread while React Native's layout pass has not yet completed (e.g., during navigation push/pop, or when the screen is hidden via display: none where our patch @shopify+flash-list+2.3.2+002+skip-layout-when-hidden.patch intentionally skips layout measurement).
    2. When the native scroll event fires, RecyclerView.js's onScroll handler invokes:
      recyclerViewManager.recordInteraction();
    3. Inside RecyclerViewManager.js, recordInteraction() directly delegates to:
      this.itemViewabilityManager.recordInteraction();
      which triggers updateViewableItems() and calls this.recyclerViewManager.getWindowSize().
    4. getWindowSize() requires an active layoutManager:
      getWindowSize() {
          if (!this.layoutManager) {
              throw new Error("LayoutManager is not initialized, window size is unavailable");
          }
          return this.layoutManager.getWindowSize();
      }
      Because layoutManager is undefined before the first measurement or after the list is hidden/dirtied, calling recordInteraction() causes an unhandled exception that crashes the hybrid app.

    What alternative solutions did you explore? (Optional)

    Catching the error with try / catch inside ExternalScrollFlashListTable.tsx only protects the synthetic scroll path of MoneyRequestReportView, but does not protect native FlashLists throughout the app (such as Workspaces_List or SearchRouter). Fixing this at the FlashList patch level protects all lists globally.

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

    Add a patch in patches/@shopify/flash-list/ (or update patch 002 / create patch 017+fix-record-interaction-without-layout.patch):

    1. In node_modules/@shopify/flash-list/dist/recyclerview/RecyclerViewManager.js (and src counterpart):
      Guard recordInteraction() using hasLayout():

      --- a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerViewManager.js
      +++ b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerViewManager.js
      @@ -215,6 +215,9 @@ export class RecyclerViewManager {
           }
           recordInteraction() {
      +        if (!this.hasLayout()) {
      +            return;
      +        }
               this.itemViewabilityManager.recordInteraction();
           }
    2. In node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js (and src counterpart):
      Also add a safeguard in the onScroll callback:

      --- a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js
      +++ b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js
      @@ -266,7 +266,9 @@ const RecyclerViewComponent = (props, ref) => {
               if (isInitialScrollComplete) {
      -            recyclerViewManager.recordInteraction();
      +            if (recyclerViewManager.hasLayout()) {
      +                recyclerViewManager.recordInteraction();
      +            }
               }
    3. Update patches/@shopify/flash-list/details.md documenting this safeguard alongside the existing scrollToIndex guard in patch 002.

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

    Add a test in tests/unit/FlashListTest.tsx (or a unit test verifying RecyclerViewManager) that triggers recordInteraction() on a newly instantiated RecyclerViewManager whose layoutManager is undefined, verifying that it safely early-returns without throwing "LayoutManager is not initialized, window size is unavailable".

  8. melvin-bot commented on Oct 21, 2026

    @melvin-bot

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

  9. melvin-bot commented on Oct 21, 2026

    @melvin-bot

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

  10. linhvovan29546 commented on Oct 21, 2026

    @linhvovan29546
    Contributor

    @MelvinBot Please analyze the issue and provide a proposal.

  11. MelvinBot commented on Oct 21, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    Our own FlashList patch 002 (skip layout when hidden) causes this. When a list first mounts at 0×0 (a screen that is hidden or not sized yet), the patch skips the first layout. So FlashList never creates its layoutManager.

    Upstream FlashList always creates it on the first layout, so upstream never hits this.

    Until a real onLayout arrives, any native scroll event crashes the list. On iOS, UIKit can send a scroll event with no user touch, for example when it adjusts the content inset under the tab bar. FlashList's onScroll then calls recordInteraction() → updateViewableItems(). That reads getWindowSize() before it checks whether the list has any viewability listeners, and getWindowSize() throws.

    Workspaces_List has no viewability callbacks, yet it still crashes because of that check order.

    Code references

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

    Extend patch 002 so the scroll path handles the "no layout yet" state it creates:

    1. In ViewabilityManager.updateViewableItems, return early when there are no viewability listeners or rvManager.hasLayout() is false. Do both checks before calling getWindowSize(). This fixes every list without viewability callbacks, including Workspaces_List.
    2. In RecyclerViewManager.recordInteraction, return early when hasLayout() is false. Then hasInteracted is not set before the list can measure anything, so waitForInteraction still works on the next scroll.
    3. In RecyclerViewManager.computeItemViewability, add a hasLayout() check. This covers lists that use onViewableItemsChanged.
    4. Update the patch 002 entry in patches/@shopify/flash-list/details.md.
    5. Add a unit test: a RecyclerViewManager with no layout does not throw from recordInteraction() or computeItemViewability(), and viewability still fires after the first layout.

    What alternative solutions did you explore? (Optional)

    • Remove patch 002. This fixes the crash but brings back the hidden-screen layout bugs the patch was added for.
    • Guard only recordInteraction, as the other proposals suggest. Lists with viewability callbacks would still crash through computeItemViewability.
    • try/catch in App components. FlashList throws before it calls our onScroll, so a try/catch in our code cannot catch it.
    Confidence and what's unverified

    The code path is confirmed. The exact iOS trigger is not verified on a device. It is either a UIKit inset adjustment or a tab-mount race on a list first measured at 0×0. Sentry breadcrumbs could confirm it: look for a tab switch or navigation just before the crash, with no onScrollBeginDrag. The fix works whichever trigger it is.


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


    view run

  12. mountiny commented on Oct 21, 2026

    @mountiny
    ContributorAuthor

    @linhvovan29546 please choose a proposal. Thank you.

  13. linhvovan29546 commented on Oct 22, 2026

    @linhvovan29546
    Contributor

    MelvinBot's proposal LGTM!

    🎀 👀 🎀 C+ reviewed

  14. melvin-bot commented on Oct 22, 2026

    @melvin-bot

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

  15. mountiny commented on Oct 22, 2026

    @mountiny
    ContributorAuthor

    @linhvovan29546 thanks! please go ahead

  16. linhvovan29546 commented on Oct 23, 2026

    @linhvovan29546
    Contributor

    @MelvinBot implement this

  17. MelvinBot commented on Oct 23, 2026

    @MelvinBot
    Contributor

    🤖 Draft PR created: #104826
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    It extends FlashList patch 002 with hasLayout() guards in updateViewableItems, recordInteraction, and computeItemViewability, as in the approved proposal. Two new unit tests fail on the old code with LayoutManager is not initialized, window size is unavailable and pass with the fix. I couldn't reproduce the iOS crash on a device, so the Tests and QA sections are still for you to fill in.


    view run

  18. changed the title [-][$175] [Sentry: APP-MHA] HybridApp iOS: FlashList onScroll throws "LayoutManager is not initialized"[/-] [+][Due for payment 2026-11-05] [$175] [Sentry: APP-MHA] HybridApp iOS: FlashList onScroll throws "LayoutManager is not initialized"[/+] on Oct 29, 2026
  19. melvin-bot commented on Oct 29, 2026

    @melvin-bot

    @linhvovan29546

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.24-8 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-11-05. 🎊

    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]

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

Metadata

Metadata

Labels

DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorOverdue

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions