Skip to content

test(dev): error in lazy module should be catchable#9975

Merged
graphite-app[bot] merged 1 commit into
mainfrom
06-26-test_dev_error_in_lazy_module_should_be_catchable
Jun 26, 2026
Merged

test(dev): error in lazy module should be catchable#9975
graphite-app[bot] merged 1 commit into
mainfrom
06-26-test_dev_error_in_lazy_module_should_be_catchable

Conversation

@sapphi-red

@sapphi-red sapphi-red commented Jun 26, 2026

Copy link
Copy Markdown
Member

This PR adds a failing test that covers a bug found in vitejs/vite#21626.

When lazy bundling is enabled, the following bug exists:

try {
  await import('./foo.js'); // throws an error inside
} catch (e) {
  // the error is not caught here and a unhandled error happens
}

sapphi-red commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

How to use the Graphite Merge Queue

Add the label graphite: merge-when-ready to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@netlify

netlify Bot commented Jun 26, 2026

Copy link
Copy Markdown

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit a855c1e
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6a3eb4252d86f00008c7040f

@sapphi-red sapphi-red marked this pull request as ready for review June 26, 2026 05:29
@sapphi-red sapphi-red requested review from h-a-n-a and hyf0 as code owners June 26, 2026 05:29

h-a-n-a commented Jun 26, 2026

Copy link
Copy Markdown
Member

Merge activity

  • Jun 26, 5:14 PM UTC: The merge label 'graphite: merge-when-ready' was detected. This PR will be added to the Graphite merge queue once it meets the requirements.
  • Jun 26, 5:16 PM UTC: h-a-n-a added this pull request to the Graphite merge queue.
  • Jun 26, 5:21 PM UTC: Merged by the Graphite merge queue.

This PR adds a failing test that covers a bug found in vitejs/vite#21626.

When lazy bundling is enabled, the following bug exists:

```javascript
try {
  await import('./foo.js'); // throws an error inside
} catch (e) {
  // the error is not caught here and a unhandled error happens
}
```

<!--
- What is this PR solving? Write a clear and concise description.
- Reference the issues it solves (e.g. `fixes #123`).
- What other alternatives have you explored?
- Are there any parts you think require more attention from reviewers?

Also, please make sure you do the following:

- Read the Contributing Guidelines at https://rolldown.rs/contribution-guide/.
- Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us review it.
- Update the corresponding documentation if needed.
- Include relevant tests that fail without this PR but pass with it. If the tests are not included, explain why.

Thank you for contributing to Rolldown!
-->
@graphite-app graphite-app Bot force-pushed the 06-26-test_dev_error_in_lazy_module_should_be_catchable branch from ba6a483 to a855c1e Compare June 26, 2026 17:17
graphite-app Bot pushed a commit that referenced this pull request Jun 26, 2026
Builds on #9975 (which added the failing test). Fixes the bug found in vitejs/vite#21626.

## Problem

With lazy compilation enabled, an error thrown while a lazily-imported module *initializes* never reached the consumer:

```js
try {
  await import('./foo.js'); // throws during init
} catch (e) {
  // never runs — instead the error escaped as an unhandled rejection,
  // and `await import(...)` resolved as if nothing went wrong
}
```

The lazy proxy wraps the import in an eagerly-created `lazyExports` async IIFE. On the first compile of the lazy chunk (the on-demand `@vite/lazy` response), the real module's initializer runs synchronously inside that IIFE and throws, so `lazyExports` rejects before any consumer can attach a handler. The rejection went unhandled, and the proxy then returned the module namespace as if the import had succeeded.

## Fix

- **`proxy-module-template.js`**: after loading the chunk, `await` the real module's own `rolldown:exports` promise instead of just handing back the namespace from `loadExports`. An error during init now rejects `lazyExports` too, so it surfaces at the consumer's `await import(...)` (catchable) rather than escaping as an unhandled rejection.

## Tests

- Rewrote the `lazy-init-error` spec to assert both cases against the same module: with `try/catch` the error is caught at `await import(...)`; with no handler it surfaces as exactly one `unhandledrejection`.
@graphite-app graphite-app Bot merged commit a855c1e into main Jun 26, 2026
33 checks passed
@graphite-app graphite-app Bot deleted the 06-26-test_dev_error_in_lazy_module_should_be_catchable branch June 26, 2026 17:21
@rolldown-guard rolldown-guard Bot mentioned this pull request Jul 1, 2026
shulaoda added a commit that referenced this pull request Jul 1, 2026
## [1.1.4] - 2026-07-01

### 🚀 Features

- disable `experimental.lazyBarrel` by default (#10071) by @shulaoda

### 🐛 Bug Fixes

- dev: disable lazy barrel in dev mode (#10060) by @shulaoda
- generate: keep full JSON interface under preserveModules namespa… (#10056) by @IWANABETHATGUY
- check finalize_other_specifiers in its own Debug attribute (#10032) by @shulaoda
- serialize the KeepAssign unused minify option as "keep_assign" (#10031) by @shulaoda
- keep fragments after the newline fragment in MagicString::last_line (#10023) by @shulaoda
- generate: undeclared JSON named exports under preserveModules (#10020) (#10027) by @IWANABETHATGUY
- deconflict: rename CJS-wrapped locals that shadow chunk-root bindings (#9921) by @IWANABETHATGUY
- rolldown: keep entry facade when a shared chunk holds another entry's module (#9997) by @hyf0
- treeshake: also bail JSON default split when the object escapes (#9996) by @IWANABETHATGUY
- don't classify await in a strict-mode function as top-level await (#9987) by @shulaoda
- avoid spurious leading newline in addon hooks (banner/footer/intro/outro) (#9989) by @shulaoda
- handle JSON default mutation bailouts (#9972) by @TheAlexLichter
- plugin: make lazy hook metadata enumerable (#9991) by @TheAlexLichter
- dev: make init errors in lazy-compiled modules catchable (#9981) by @h-a-n-a
- treeshake: keep computed-key side effects on namespace member access (#9986) by @shulaoda
- binding: validate replace plugin delimiters length instead of panicking (#9984) by @shulaoda
- reconstruct nested rest patterns in into_expression (#9980) by @IWANABETHATGUY
- reconstruct rest patterns as spread in into_expression (#9976) by @shulaoda
- preserve export keyword on multi-declarator exports under keepNames (#9974) by @shulaoda
- deterministically keep the shortest name for deduplicated assets (#9948) by @x1024
- treeshake: apply @__NO_SIDE_EFFECTS__ to cross-chunk namespace calls (#9960) by @IWANABETHATGUY

### 🚜 Refactor

- drop redundant program scope enter/leave in finalizer (#10049) by @shulaoda
- deconflict: extract collect_chunk_scope_captured_names (#10006) by @IWANABETHATGUY
- unify pre-scan multi-declarator split into one decision site (#9982) by @IWANABETHATGUY
- common: return bool from SymbolRef::is_not_reassigned (#9962) by @IWANABETHATGUY

### 📚 Documentation

- rolldown: remove outdated comment for removing parenthesized expression (#10062) by @Dunqing
- use GitHub-flavored alert for Etiquette note in contribution guide (#10012) by @IWANABETHATGUY
- replace: explain the delimiters left and right boundaries (#9985) by @shulaoda
- ast-mutation: remove stale Address Use section after pre-scan refactor (#9983) by @IWANABETHATGUY
- remove fathom (#9968) by @mdong1909
- contribution-guide: code-format main branch references (#9966) by @IWANABETHATGUY
- contribution-guide: fix stale REPL note and tidy wording (#9957) by @hyf0
- contribution-guide: clarify when to discuss before opening a PR (#9955) by @hyf0

### ⚡ Performance

- disable preserve_parens across all parse paths (#10057) by @Dunqing
- common: inline declared_symbols with SmallVec (#9920) by @IWANABETHATGUY
- common: pack TaggedSymbolRef into 8 bytes (#9919) by @IWANABETHATGUY
- sourcemap: skip newline scan on the no-sourcemap join fast path (#9936) by @Boshen

### 🧪 Testing

- dev: error in lazy module should be catchable (#9975) by @sapphi-red
- dev: reject unknown lazy compile modules (#9969) by @sapphi-red

### ⚙️ Miscellaneous Tasks

- deps: update actions/cache action to v6 (#10001) by @renovate[bot]
- trigger vite ecosystem-ci from PR comments (#10058) by @shulaoda
- deps: update napi to v3.10.0 (#10063) by @renovate[bot]
- remove unused From impl for RolldownLabelSpan (#10055) by @shulaoda
- remove dead Diagnostic::with_kind method (#10054) by @shulaoda
- remove unused StatementExt methods (#10053) by @shulaoda
- remove unused ExpressionExt methods (#10052) by @shulaoda
- remove commented-out re_export_all_names field (#10051) by @shulaoda
- deps: update pnpm to v11.9.0 (#10047) by @renovate[bot]
- remove the unused BindingGenerateHmrPatchReturn napi type (#10034) by @shulaoda
- remove the dead inline_entry_chunk_wrapping scaffolding (#10037) by @shulaoda
- deps: bump oxc_resolver to 11.22.0 (#10045) by @Boshen
- remove never-constructed MatchImportKind::_Ignore variant (#10041) by @shulaoda
- remove the unused ScheduledBuild napi struct (#10033) by @shulaoda
- remove dead compute_hmr_update_single method (#10040) by @shulaoda
- drop the redundant visited.insert in manual code splitting (#10038) by @shulaoda
- remove the dead output_assets vector in render_chunk_to_assets (#10036) by @shulaoda
- remove the unused From<String>/Display impls for BindingLogLevel (#10035) by @shulaoda
- deps: upgrade oxc to 0.138.0 and migrate to per-type AST construction (#10018) by @shulaoda
- deps: update rust crates (#9911) by @renovate[bot]
- deps: update test262 submodule for tests (#10016) by @rolldown-guard[bot]
- deps: update github actions (#9999) by @renovate[bot]
- deps: update npm packages (#10000) by @renovate[bot]

### ◀️ Revert

- "fix(plugin): make lazy hook metadata enumerable (#9991)" (#10005) by @shulaoda

### ❤️ New Contributors

* @x1024 made their first contribution in [#9948](#9948)

Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>
graphite-app Bot pushed a commit that referenced this pull request Jul 2, 2026
### Description

Rewrites `internal-docs/lazy-compilation/{design,implementation}.md` to match the current implementation. The docs dated back to the initial POC (split out in #9826) and had drifted from the code in several load-bearing places. Every claim was re-verified against current `main` (file/line spot-checks, cross-checked with the playground specs).

#### Corrections to stale / wrong content

- **Proxy templates**: both inline code blocks are now the real `proxy-module-template{,-fetched}.js` verbatim. The stub evicts its runtime cache entry and ends with `return await loadExports($STABLE_PROXY_MODULE_ID)['rolldown:exports']` — the two-level promise chain from #9981 that makes init errors catchable at the consumer's `await import()`. The fetched template returns `loadExports($STABLE_MODULE_ID)` instead of the import namespace (export-name preservation in shared chunks, #9132).
- **Module IDs**: design.md's TL;DR claimed absolute paths are used "consistently throughout the runtime" — runtime lookups actually use stable (cwd-relative) ids; absolute paths survive only in the `/@vite/lazy?id=` param and the fetched template's `import($MODULE_ID)`. Also documents the four template placeholders and their JSON-quoted rendering (#9102).
- **Error handling**: replaced "`Err` or panic is fine for POC" with the actual contract — unknown ids are rejected as a cache-key-only security gate (#9969), napi surfaces `Failed to compile lazy entry:` → HTTP 500, and init errors are catchable on both cold and warm paths (#9975/#9981).
- **Dedup**: the race-condition section's "potential future enhancement" runtime guard is long implemented — lazy chunks render `createEsm/CjsInitializer(stableId, factory, 1)` with a dedup flag (HMR patches deliberately omit it so updates re-execute). Rewrote the section around the two-layer dedup: per-client `executed_modules` pruning + the runtime flag.
- **Rebuild id normalization**: the old note said the `ModuleChanged` rebuild "should resolve or normalize" the proxy id to a real module id — the raw proxy id is deliberately correct (it is the incremental-cache invalidation key for the proxy whose content changed; normalizing would leave the stale stub cached). Note removed and the actual mechanism documented.
- Misc: `lazyMagic` helper (never existed), `__export` → `__exportAll`, `snippet` → `ast_factory` in the Lessons Learned snippets, `registerModule(stableId, { exports })` shape.

#### New coverage

- **Enabling / wiring**: `experimental.devMode.lazy: true`, inner-plugin registration, the shared `LazyCompilationContext` handed to the `DevEngine`
- **Lazy chunk rendering**: initializer wrappers + `__rolldown_module_id__` param, the nested-lazy `import()` → `/@vite/lazy` rewrite, entry init call, `lazy_compile_{n}.js` naming
- **Emitted assets**: delivered via `onAdditionalAssets` before the code returns (#9815)
- **Build output refresh**: `update_watch_paths()` runs first (what makes post-fetch edits watchable at all), silent output swap for connected clients, failure paths (#9903)
- **Editing a fetched lazy module**: per-client HMR outcomes; a non-accepting proxy escalates to FullReload → HmrRebuild with a deferred reload
- **Client sessions**: implicit creation on `hmr:module-registered`, removal on ws disconnect, the `"rolldown-tests"` escape hatch; clientId prunes the patch — nothing is "routed"
- **Known limitations**: link-stage-synthesized exports (JSON/text/base64/dataurl register `{}` inside lazy chunks until rebuild + refresh), CSS error deferral, assets require load-hook plugins, sourcemaps (inline-only for lazy chunks — the map asset is discarded on the lazy path, unlike `HmrPatch`)
- **Test coverage table**: the 7 playground specs + `dev-lazy-compile.test.ts` and what each pins

Docs only — no runtime change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants