Skip to content

fix(DropdownMenu): a failed sheet preload is handled and retried, never an unhandled rejection - #8478

Merged
cixzhang merged 4 commits into
meta:mainfrom
vjeux:fix/menu-sheet-preload-rejection
Oct 30, 2026
Merged

cixzhang merged 4 commits into
meta:mainfrom
vjeux:fix/menu-sheet-preload-rejection

Conversation

@vjeux

@vjeux vjeux commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

User impact

A bottom-sheet DropdownMenu preloads its action list at rest. When that load fails (a dropped or flaky connection), the page gets an unhandled promise rejection in the console. The rejected load also stays cached, so every later open of the menu fails to show its rows until a reload. In this repository's CI, the same unhandled rejection fails the UI test shard that runs DropdownMenu.ssr.test.tsx while every test passes, as on #8353, #8229 and #8211.

Problem and solution fit

  • Why does it occur? preloadMenuBottomSheetActionList starts the load with void loadActionList(): nothing handles a rejection, and actionListLoad ??= keeps the rejected promise.
  • Why does it harm the task? An unhandled rejection is reported as an error, and a cached rejection turns one network blip into a menu whose rows never appear.
  • Why does that matter? The preload only exists to make the first open instant; it must never break the open. In tests, the SSR file can finish while its preload is still importing; the environment tears down mid-import and the rejection fails the run.

The fix handles the preload's failure and drops the failed load, so the next preload or open asks the loader again. The SSR test waits for the sheet modules before it ends.

Expected behavior and authority

The sheet loads on demand (#7709) and preloads at rest so the first open shows its actions at once (#8392). A preload is an optimization: its failure surfaces no error and blocks no retry.

Smallest restoration

  • loadActionList clears its cached load when the import rejects.
  • preloadMenuBottomSheetActionList handles the rejection.
  • DropdownMenu.ssr.test.tsx awaits the two sheet modules after unmounting.

No API, rendering or timing change.

Evidence

  • Reproduction before the change: on main e9b3f3e, DropdownMenu.ssr.test.tsx run alone fails 5 of 6 runs with three EnvironmentTeardownErrors each, and the new unit test records the unhandled rejection.
  • Result after the change: the SSR file passes 8 of 8 runs alone, and the unit test passes.
  • Representative unchanged path: all 10 DropdownMenu test files (312 tests) pass.
  • Regression test or other mutation-sensitive proof: lazyMenuBottomSheet.test.ts fails the list's import once, then checks there is no unhandled rejection and that the next call imports it again. Keeping the rejected load cached fails it ("expected 1 to be 2").

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

  • At 3962530: the changeset and comments promise only what holds everywhere (no unhandled rejection, no kept failure); the unit and SSR tests: 2 passed; changeset coverage: passes.
  • At 8877d94 (adds the Changeset): node scripts/release/changeset-coverage.mjs pr --base e9b3f3edac fails at 4d8e178 and passes here ([fix], @astryxdesign/core); pnpm check:changesets: valid.
  • pnpm exec vitest run --project ui packages/core/src/DropdownMenu/lazyMenuBottomSheet.test.ts: fails on main, passes here.
  • pnpm exec vitest run --project ui packages/core/src/DropdownMenu/DropdownMenu.ssr.test.tsx, alone, 8 times: 8 of 8 (main: 1 of 6).
  • pnpm exec vitest run packages/core/src/DropdownMenu: 10 files, 312 tests.
  • tsc -p packages/core/tsconfig.json --noEmit, eslint and prettier on the changed files: clean.

…er an unhandled rejection

A sheet menu preloads its action list at rest with a fire-and-forget import.
When that import failed (a dropped connection) the rejection went unhandled,
an error in the console, and the rejected promise stayed cached, so every
later open failed too. In tests, the SSR file could end while the preload was
still loading: the environment tore down mid-import, and the run failed on
unhandled errors with every test passing.

The preload now handles its own failure, and a failed load is not kept, so
the next preload or open loads the list again. The SSR test waits for the
sheet modules before it ends. A unit test fails a preload once and checks
there is no unhandled rejection and that the next call loads again.
@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Error Error Oct 28, 2026 3:33pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 28, 2026, 3:48 PM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 26, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix itself is sound, but please correct the changeset before merging. Handling the preload's rejection and dropping the failed load both work, and the new unit test fails without either part. The changeset (and the matching comments in lazyMenuBottomSheet.ts) promises that the next open loads the actions again, yet Chromium 149 and Firefox 151 do not fetch a module again once its native import() has failed, so in builds that load the action list as a native ES module chunk the open still fails until a reload. Please narrow the changeset to what holds everywhere (no unhandled rejection, and the failed load is no longer kept), or make the open path survive a failed import.

[Automated review]

'@astryxdesign/core': patch
---

[fix] A bottom-sheet DropdownMenu whose sheet preload fails (a dropped connection) no longer reports an unhandled promise rejection, and its next open loads the sheet's actions again instead of failing until a reload. (#8478)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This promises that the next open loads the actions again, but clearing actionListLoad only lets the next import() call happen: Chromium 149 and Firefox 151 reject a second import() of a module URL whose fetch failed without fetching it again, so in a build that loads the list as a native ES module chunk the open still fails until a reload. Suggest stating what holds everywhere, for example "no longer reports an unhandled promise rejection, and no longer keeps the failed load, so a module loader that retries loads the actions on the next open". The comments in lazyMenuBottomSheet.ts at lines 18-19, 59-60 and 69-71 say the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3962530: the changeset and the three comments now promise only what holds everywhere, no unhandled rejection and no kept failure, so a module loader that retries loads the actions on the next open; the unit test's title says it asks the loader again.

…in; it does not promise a fresh load

A browser does not fetch a native module again once its import failed, so clearing the failed load only lets the next open ask the loader again: a loader that retries then loads the actions. The changeset, the three comments and the unit test's title now say that.
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Preview availability: Storybook and Sandbox are not both ready on the exact-head Vercel preview.

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/ContextMenuItem 53.5KB 53.5KB 0B 9.8KB 9.8KB 0B
core/DropdownMenu 73.4KB 73.4KB +13B 15.0KB 15.0KB 0B
core/DropdownMenuCheckboxItem 16.4KB 16.4KB 0B 5.7KB 5.7KB 0B
core/DropdownMenuDivider 6.6KB 6.6KB 0B 3.6KB 3.6KB 0B
core/DropdownMenuGroup 6.1KB 6.1KB 0B 3.3KB 3.3KB 0B
core/DropdownMenuItem 15.6KB 15.6KB 0B 5.3KB 5.3KB 0B
core/DropdownMenuRadioGroup 6.0KB 6.0KB 0B 3.2KB 3.2KB 0B
core/DropdownMenuRadioItem 16.4KB 16.4KB 0B 5.7KB 5.7KB 0B
core/DropdownMenuSubMenu 58.7KB 58.7KB 0B 9.7KB 9.7KB 0B
core/MoreMenu 73.7KB 73.7KB +11B 15.0KB 15.0KB 0B
lab/CommentThread N/A 216.8KB N/A N/A 2.4KB N/A
lab/Comments N/A 221.4KB N/A N/A 2.5KB N/A

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 35.4KB 35.4KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

Status: No visual change across 4 compared shot(s).


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ready to merge: the preload's rejection is now handled, the failed load is dropped so a module loader that retries recovers on the first open, and the changeset now claims exactly that. The new unit test fails on main and with either half of the fix reverted, and DropdownMenu.ssr.test.tsx no longer fails on teardown when run alone. One optional wording nit on the header comment of lazyMenuBottomSheet.ts: after a failed open, React's lazy() keeps that failure for menus already mounted, so "the next preload or the first open" would be exact.

[Automated review]

Comment on lines +18 to +19
* The action list preloads at rest; a failed load is not kept, so the next
* preload or open asks the module loader again (a loader that retries then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this holds after a failed preload, but after a failed open React's lazy() keeps that failure, so another bottom-sheet menu that is already mounted opens straight into it without asking the loader again (as on the base). "the next preload or the first open asks the module loader again" would match the comment on preloadMenuBottomSheetActionList below.

At the maintainer's request. DropdownMenu.ssr.test.tsx takes main's version: main's test (meta#8492, meta#8685) already waits for both on-demand sheet modules before the hydrated root unmounts, which covers the wait this pull request had added there. Nothing else conflicts.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks ready to merge: the preload's rejection is now handled and the failed load is dropped, so a module loader that retries recovers on the first open, and the new unit test fails on main and with either half of the fix reverted. The changeset claims exactly that, and nothing for native ES module imports, which Chromium and Firefox do not fetch again after a failure. One optional cleanup: the description still lists a DropdownMenu.ssr.test.tsx change, but after merging main that file is main's own version (#8492 already waits for the sheet modules), so that bullet and its run counts no longer describe this diff.

[Automated review]

@vjeux
vjeux marked this pull request as draft October 30, 2026 03:37
@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Oct 30, 2026
@cixzhang
cixzhang marked this pull request as ready for review October 30, 2026 21:33
@cixzhang
cixzhang merged commit fd0aba8 into meta:main Oct 30, 2026
113 of 115 checks passed
@vjeux
vjeux deleted the fix/menu-sheet-preload-rejection branch October 30, 2026 22:41
cixzhang pushed a commit that referenced this pull request Oct 31, 2026
…t, not an empty catch (#9069)

Follows #8478.

## Maintainer impact

#8478 stopped a failed preload of a bottom-sheet DropdownMenu's action list from raising an unhandled rejection, using `loadActionList().catch(() => {})`. An empty handler hides every error in that chain, not only the expected failed load, so a real defect there would go unnoticed.

What led to it: a sweep of recent changes for error handlers that catch and default.

## Intended invariant

A sheet preload's failure is its result, never a rejection and never swallowed: `preloadMenuBottomSheetActionList` resolves `{isLoaded: true}`, or `{isLoaded: false, error}` with the loader's error. As before, a failed load is not kept, so the next open loads the list.

## Change

- `lazyMenuBottomSheet.ts`: the preload returns that result (an `ActionListPreload` type, internal to DropdownMenu).
- `DropdownMenu.tsx`: its effect starts the preload with `void`; after a failure, the first open loads the list.
- `lazyMenuBottomSheet.test.ts`: checks the result, no unhandled rejection, and a retry on the next call.
- A `[chore]` changeset: callers see no change.

Options considered:

- Keep the empty handler: it hides errors other than the expected one.
- Log the failure: a console line is no contract, and the first open already surfaces a real failure through `lazy()`.
- Return the failure as the result (chosen): the expected failure becomes part of the contract, and nothing is dropped.

Prior art: no open pull request touches `preloadMenuBottomSheetActionList`, `lazyMenuBottomSheet.ts` or an `ActionListPreload` type (searched by those names and "sheet preload").

## Evidence

- On main fd0aba8, the updated `lazyMenuBottomSheet.test.ts` fails; it passes here.
- The rest of DropdownMenu's tests pass unchanged, within `pnpm test:ui` (16,454 passed).

## Scope

- [x] No intended public API, product behavior, visual direction, or policy change.
- [x] Product changes discovered during the work were removed or split.
- [x] Public text and artifacts contain no internal Meta context.

## Testing

`pnpm exec vitest run packages/core/src/DropdownMenu/lazyMenuBottomSheet.test.ts`: fails on main, passes here.

Local gates at 45046fe: build, typecheck, lint, `pnpm test:ui` (16,454), `pnpm test:node` (all but #8520's two known `cache.test.mjs` failures, and two of main's load-sensitive cases, `ci-scope-pr-diff.test.mjs` and `component-size.test.mjs`: 30 s timeouts at box load 70–85; they pass alone on main fd0aba8 and this head), CLI (47), Storybook, the DropdownMenu and Menu accessibility specs in Chromium (228), Prettier, changeset coverage, and the knowledge, changeset and repo checks.

This branch had an error being deployed

1 failed deployment
Preview — f9b98fd4 Deployed Oct 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants