Skip to content

#20221 module existence check breaks interceptModuleExecution recovery for missing modules #20475

@lukesandberg

Description

@lukesandberg

Bug Description

The module existence check added in #20221 (shipped in v5.104.0) changes the observable control flow of __webpack_require__ in a way that breaks plugins relying on interceptModuleExecution to handle missing modules gracefully.

What is the current behavior?

When output.pathinfo is enabled (default in development), the new check in __webpack_require__ throws before interceptModuleExecution runs:

function __webpack_require__(moduleId) {
  // 1. Cache check
  var cachedModule = __webpack_module_cache__[moduleId];
  if (cachedModule !== undefined) return cachedModule.exports;

  // 2. NEW: pathinfo existence check — throws here
  if (__webpack_modules__[moduleId] === undefined) {
    var e = new Error("Cannot find module '" + moduleId + "'");
    e.code = 'MODULE_NOT_FOUND';
    throw e;
  }

  // 3. Create module
  var module = __webpack_module_cache__[moduleId] = { ... };

  // 4. interceptModuleExecution — never reached
  var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
  __webpack_require__.i.forEach(function(handler) { handler(execOptions); });
  ...
}

Previously (≤v5.103), when __webpack_modules__[moduleId] was undefined, execution reached step 4. The factory was passed as undefined in execOptions.factory, and plugins hooking interceptModuleExecution could detect this and recover (e.g., trigger a page reload instead of crashing).

What is the expected behavior?

The interceptModuleExecution hook should still have a chance to handle missing modules. Either:

  1. Move the existence check after interceptModuleExecution runs (so plugins can intervene), or
  2. Move it after execOptions.factory is read but before .call(), so the check still fires but only when no interceptor handled it, or
  3. Gate the check on whether interceptModuleExecution is in use — if interceptors are registered, skip the early throw and let the interceptor handle it.

How is this affecting a real-world project?

Next.js uses interceptModuleExecution in its ReactRefreshWebpackPlugin to detect missing module factories and recover with a page reload:

// If the original factory is missing, e.g. due to race condition
// when compiling multiple entries concurrently, recover by doing
// a full page reload.
'if (!originalFactory) {',
Template.indent('document.location.reload();'),
Template.indent('return;'),
'}',

This handles a legitimate scenario in development: during HMR with concurrent server component rendering, React's Flight protocol can serialize client references from one page that aren't present in another page's webpack module registry. The old behavior allowed the React Refresh interceptor to catch this and reload; the new check makes it a hard error before the interceptor runs.

Link to Minimal Reproduction and step to reproduce

checkout vercel/next.js#89569 which updates webpack for next.js to 5.105

pnpm i
pnpm build
pnpm test-dev-webpack test/development/app-dir/hmr-iframe/hmr-iframe.test.ts

Expected Behavior

the test should continue passing as interception hooks should be able to run before the dev mode missing module check

Actual Behavior

loading a missing module immediately fails with an Error

Environment

- webpack: 5.105.0 (also affects 5.104.0+)
- Node.js: v22.x
- OS: macOS

Is this a regression?

Yes (please specify version below)

Last Working Version

v5.98.0

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions