Skip to content

RuntimeModule.updateHash falls back to the error message when generate() throws, so the persistent cache serves a stale runtime module (ESM output, 5.104–5.109) #22026

Closed

Description

@Anonym-tsk

Bug Description

RuntimeModule.updateHash hashes a runtime module by its generated code, but when generate() throws it hashes err.message instead:

try {
  hash.update(this.getGeneratedCode());
} catch (err) {
  hash.update(err.message);
}

With experiments.outputModule and a [contenthash] in output.filename, ModuleChunkLoadingRuntimeModule.generate() (the import chunk loading module) resolves its own filename through compilation.getPath(..., { contentHashType: "javascript" }). During hashing content hashes do not exist yet, so TemplatedPathPlugin throws Path variable [contenthash] not implemented in this context. The hash of the module therefore collapses to a constant string that does not depend on the generated code at all.

That hash is the etag of the Compilation/codeGeneration cache entry. With cache.type: "filesystem" every later build gets whatever code of the module the cache holds, regardless of what the chunk graph looks like now.

We hit this in production: a commit removed the last webpackPreload hint reachable from an async chunk. The chunk preload function module (__webpack_require__.H = {}) was correctly gone, but the cached import chunk loading module still installed __webpack_require__.H.j = …. The runtime threw on that statement before __webpack_require__.C was assigned, and every page failed with __webpack_require__.C is not a function.

Link to Minimal Reproduction and steps to reproduce

Four files, no loaders (package.json has "type": "module"):

// webpack.config.cjs
const path = require('path');
module.exports = {
  mode: 'production',
  entry: { main: './src/main.js' },
  output: { path: path.join(__dirname, 'dist'), module: true, filename: '[name].[contenthash].js', clean: true },
  experiments: { outputModule: true },
  optimization: { runtimeChunk: 'single', minimize: false },
  cache: { type: 'filesystem', cacheDirectory: path.join(__dirname, '.cache') },
};
// src/main.js
import('./lazy.js').then((m) => m.run());
// src/lazy.js
export function run() { import(/* webpackPreload: true */ './sentry.js').then((m) => m.init()); }
// src/sentry.js
export function init() { console.log('sentry'); }
  1. rm -rf .cache dist && webpack — dist/runtime.*.js contains both __webpack_require__.H = {} and __webpack_require__.H.j = …, as expected.
  2. Remove /* webpackPreload: true */ from src/lazy.js and run webpack again on the same cache.

Expected Behavior

The runtime chunk contains neither __webpack_require__.H = {} nor __webpack_require__.H.j (this is what a build with a fresh cache produces).

Actual Behavior

The runtime chunk contains __webpack_require__.H.j = … from the previous build and no __webpack_require__.H = {}. Loading it throws TypeError: Cannot set properties of undefined (setting 'j') on the first statement of the chunk-loading section; nothing after it in the runtime runs.

Wrapping generate of the import chunk loading module confirms it: during _createModuleHash it throws Path variable [contenthash] not implemented in this context: [name].[contenthash].js, and at code generation time generate is never called because the cache hits.

Environment

  • webpack 5.104.1, 5.105.4, 5.107.2, 5.109.2 reproduce.
  • webpack 5.110.0 and 5.110.3 do not: RuntimeTemplate.chunkRootOutputDir now replaces hash tokens with a probe before calling getPath, so generate() no longer throws while hashing.
  • Node 24, macOS.

Is this a regression?

No. But the fallback in RuntimeModule.updateHash is still there in 5.110, so any runtime module whose generate() throws during hashing will again produce a stable-but-meaningless hash and a poisoned persistent cache. A hash that ignores the module content seems worse than failing loudly or marking the module uncacheable (e.g. treating a throwing generate() as fullHash, or hashing the runtime requirements and chunk graph state the module depends on).

Additional Context

The stale entry survives across commits in CI when the cache directory is restored, so the broken runtime shows up only in the build after the one that removed the hint, and only where the cache is warm; a clean checkout does not reproduce it. Users of 5.104–5.109 with ESM output and a persistent cache may want to know that a hashed filename plus any change in prefetch/preload usage can produce a runtime chunk that does not boot.

Activity

  1. added
    area: cachePersistent and memory caching, serialization (lib/cache, lib/serialization)
    area: runtimeEmitted runtime and chunk loading across targets (lib/runtime, lib/web, lib/node, lib/esm)
    on Sep 9, 2026
  2. devtechedge commented on Sep 9, 2026

    @devtechedge

    Working on a focused fix so RuntimeModule.updateHash does not fall back to hashing err.message when generate() throws during content-hash resolution - that collapses the filesystem cache etag and can serve a stale runtime module. Will add a regression test and open a PR linking this issue.

  3. devtechedge commented on Sep 9, 2026

    @devtechedge

    Stepping back after a closer look: the reported ESM preload/contenthash reproduction is already fixed on current main via RuntimeTemplate.chunkRootOutputDir, and changing the updateHash error fallback (fullHash promotion vs failing loudly vs hashing dependency state) looks like a product/cache-policy choice that needs maintainer direction before a PR. Happy to pick it back up if maintainers want a specific approach.

  4. self-assigned this
    on Sep 10, 2026
  5. hai-x commented on Sep 10, 2026

    @hai-x
    Member

    webpack 5.110.0 and 5.110.3 do not: RuntimeTemplate.chunkRootOutputDir now replaces hash tokens with a probe before calling getPath, so generate() no longer throws while hashing.

    Yeah, i test it and couldn't repro it in the latest main. I think I need to dig into it deeper for the background of the fix.

  6. added a commit that references this issue on Sep 10, 2026
    4f3f215
  7. Pcmhacker-piro commented on Sep 10, 2026

    @Pcmhacker-piro
    Contributor

    Hi @Anonym-tsk,

    I have submitted a fix for this in #22032.

    The PR removes the try-catch fallback to err.message in RuntimeModule.prototype.updateHash. When generate() throws during hashing, the failure now cleanly propagates to Compilation._createModuleHash to record a ModuleHashingError (failing loudly and accurately) rather than collapsing the module's hash into a static error string that can poison persistent filesystem caches.

    Feel free to check out #22032!

  8. Anonym-tsk commented on Sep 10, 2026

    @Anonym-tsk
    Author

    I tried #22032 against the reproduction above and against the real project that hit this in production (ESM output, [contenthash] filenames, runtimeChunk: 'single', cache.type: 'filesystem').

    webpack change result
    5.105.4 #22032 applied minimal repro no longer builds: ModuleHashingError for webpack/runtime/import chunk loading and webpack/runtime/publicPath (publicPath: 'auto'), both "Path variable [contenthash] not implemented in this context"
    5.104.1 #22032 applied the production build of our app fails the same way, one error for import chunk loading
    5.110.3 #22032 applied, minimal repro, two builds on one cache both pass; the second runtime has neither __webpack_require__.H = {} nor __webpack_require__.H.j
    5.110.3 #22032 applied, our app, two production builds on one cache both pass; the second runtime contains no preload code at all (cold 168 s, warm 28 s, so the cache is doing its job)

    So from my side:

    • The direction of fix(runtime): throw when runtime module code generation fails in updateHash #22032 is right. Failing the build is strictly better than hashing err.message: with it, the build that broke our production would simply not have compiled.
    • On its own it is a breaking change for 5.104–5.109: every ESM build with a hashed filename stops compiling, because ModuleChunkLoadingRuntimeModule (and AutoPublicPathRuntimeModule) throw while hashing. It is only safe together with RuntimeTemplate._chunkNameShape / chunkRootOutputDir from 5.110.0, which is already on main. It cannot be backported without that.
    • The try/catch was introduced in 0f08918 ("improve hashing of runtime modules that need a full hash") with a comment that the compilation hash may not be ready during hashing, i.e. it deliberately tolerated generate() throwing for fullHash modules. No built-in module relies on that today, but a third-party runtime module might, so "no breaking change" in the PR description is optimistic.
    • Compilation._createModuleHash still assigns the constant "XXXXXX" on error. The build fails, so the cache is not poisoned, but if that error is ever downgraded to a warning the original problem comes back through the same door.

    Happy to re-run either scenario if a different approach is chosen.

  9. added a commit that references this issue on Sep 10, 2026
    a73d3c6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area: cachePersistent and memory caching, serialization (lib/cache, lib/serialization)area: runtimeEmitted runtime and chunk loading across targets (lib/runtime, lib/web, lib/node, lib/esm)

Type

No type

Fields

Priority

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions