Repository navigation
dev server: import() runs the module after the calling code and rejects when it fails - #46770
Conversation
…and reject when the load fails
|
Updated 12:59 PM PT - Oct 24th, 2026
✅ @robobun, your commit 55e710d12b109b2446c62f5fcc1e48363bd7f6bc passed in 🧪 To try this PR locally: bunx bun-pr 46770That installs a local version of the PR into your bun-46770 --bun |
|
Status Reproduced with Local runs with the debug build on Linux x64, base This PR is a draft on purpose. It is stacked on #46764 and it asks one question (Notes in the body): is the later start of a module with top-level await wanted. PR: #46770 |
Stacked on #46764. Do not merge this one first.
Problem
import("./m")runsminside the call.import("./bad").catch(h)throwsError: boomat the call, andmmisses state that the caller sets after it.HMRModule.dynamicImport(src/runtime/bake/hmr-module.ts:199) calls the loader first, which evaluates the module at once.Fix
dynamicImportisasync. It converts the specifier to a string, waits one microtask, then calls the loader, as Node and plain bun order it. A failed load rejects the promise.test/bake/hmr-module.test.ts(14 new cases, 13 fail on the base) and 5 new dev tests. All oftest/bakepasses.Background
import(x)of a dev-server module tohmr.dynamicImport(id), in the browser and on the server.React.lazy). dev server: re-evaluate every module between a saved file and its accepting module #46764 evaluates that caller again after the fix. Without it, a failed "use client" component stays at 500 until restart.Downsides
import()now ends the process (unhandled rejection). Before, the route answered 500.import()costs 4 or 5 heap cells and 7,551 to 8,303 instructions more (JIT off), and settles one microtask later.Notes
Question for a maintainer. The header of
hmr-module.tsnamesbun buildas the model. A bundle without code splitting starts a module with top-level await inside theimport()call (init_x().then(...)). After this PR the dev server starts it after the calling code, as Node, plain bun and a bundle with--splittingdo.test/bake/dev-and-prod.test.tspins both orders. If the dev server must keep the in-call start for such a module, the entry needs a gate on the module's async flag.Tests (base means
src/of #46764 with the same debug build)test/bake/hmr-module.test.ts,describe("import()"): 14 cases. 13 fail on the base. The one that passes is a control (an id that is not bundled goes to the runtime'simport()).test/bake/dev/esm.test.ts: "import() runs a bundled module after the current code" and "import() of a module that fails to load rejects". Both fail on the base.test/bake/dev/hot.test.ts: "import() in a module that a hot update runs again" and "a module whose import() failed is loaded again when it is fixed". Both fail on the base.test/bake/dev-and-prod.test.ts: one fixture in development and in a production build. The development half fails on the base.test/bake/dev/react-response.test.ts: two guards with real React. A "use client" component that fails to load on the server is rendered after it is fixed (500, 500, fix, 200). A page thatlazy()loads and that fails is shown after it is fixed, with no page reload. Both pass on the base and here. With the same entry on main, which has no dev server: re-evaluate every module between a saved file and its accepting module #46764, the first one stays at 500 after the fix.await 0fails 10 of the 14 cases. Removal of the string conversion fails 1.test/bake/dev/production.test.tstimes out at the 5 s default in up to 9 of its 21 tests on this machine, with and without this change. All 21 pass with--timeout 60000.Measurements (base against this branch,
hmr-module.tsbundled alone for the server, bun 1.4.3)import(): loaded 2 to 6, first load 15 to 19, first load with top-level await 32 to 37, not bundled 18 to 23 (heapStats().objectTypeCountsover 1,000 calls, no GC in the window).bun buildbundle: body@1 then@2, base: 0 and 1). Loaded then@2 (base 1, bundle 2). Top-level await then@6 (base 4). Not bundled then@11 (base 9).import(), JIT off, one GC thread: loaded 5,251 to 13,008, first load 36,909 to 44,460, not bundled 31,525 to 39,828.perfandvalgrindare not installed here. A wrapper aroundperf_event_opencounted user-mode instructions.bake-codegen.ts --debug=OFF: client 55,309 to 55,315 B, server 12,684 to 12,690 B. Client as embedded (zstd 19): 18,676 to 18,683 B.What changes, by case
toStringof the specifier that throws, exports that cannot become a namespace.Acallsimport("./B")andBimportsA:Bnow runs afterAis complete. Before,Breadnull. A module that callsimport()of itself gets its namespace.bun:afterUpdatefires before the body of a module that a hot update loads for the first time throughimport()..then()caller no longer has the importer in the stack trace of a failed load. A caller that awaits theimport()has it (at async <importer>).Not changed here
import(x)whilexis suspended on a top-level await resolves withnull, and so doesawait import()of the module itself. The loader gives such a load the module as it is.import(x, options)of an id that is not bundled resolves withundefined. dev server: return the promise from import() with options #46683 adds the missingreturn.import()of a specifier that a plugin'sonResolvefilter matched, and of a stylesheet, is printed asPromise.resolve().then(() => hmr.require(id))and does not reachdynamicImport.hmr.require(object)still registers a module under an object key.Self-review. The review ran on an earlier shape of this change (a synchronous entry with a fast path for loaded modules). Of its 32 concerns, 21 are addressed here or by #46764: the smaller
asyncbody, the importer that is evaluated again after a fix, the second importer of a failed module, the tests in both modes, the React tests, the stack test. 2 no longer apply, because the fast path and the edit of the options helper are gone. 4 name code that this PR does not change (see "Not changed here"). 5 are about demand and approval: no user report, and no maintainer has said yes to the later start yet (see the question above).Provenance. No issue reports these faults. They come from an internal fuzz run against Node, plain bun and
bun build. #37734 made the call reject and was folded into #39488, which is not merged.no test proof · iteration 3 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bake/dev/hot.test.ts, test/bake/dev/esm.test.ts, test/bake/dev-and-prod.test.ts