Skip to content

DefinePlugin: non-optional property access past an undefined member incorrectly evaluates to undefined instead of throwing TypeError #22014

Closed

Description

@Pcmhacker-piro

Have you used AI?

No

Which part of webpack is affected?

parser (JavaScript parsing, import/export analysis)

Bug Description

In DefinePlugin, when a defined object member chain resolves to undefined (for example, OBJECT.SUB.UNKNOWN), accessing properties non-optionally past that undefined member (such as OBJECT.SUB.UNKNOWN.a, OBJECT.SUB.UNKNOWN["a"], OBJECT.SUB.UNKNOWN.a.b, or OBJECT.SUB.UNKNOWN.a?.b) causes webpack's expressionMemberChain hook to collapse the entire expression into undefined.

At runtime in native JavaScript, accessing a property on undefined (undefined.a) must throw TypeError: Cannot read properties of undefined (reading 'a'). Because webpack replaces the entire expression with (undefined), the code silently evaluates to undefined without throwing a TypeError.

While PR #21827 recently addressed this for function calls in parser.hooks.callMemberChain (by using dropTrailingMembers to only replace up to the undefined member so undefined.a?.() is emitted and the trailing read off undefined throws), parser.hooks.expressionMemberChain was not updated and still unconditionally replaces the complete expression with (undefined).

Link to Minimal Reproduction and steps to reproduce

Minimal reproduction script using webpack Node API:

import webpack from 'webpack';
import fs from 'fs';

// entry.js
fs.writeFileSync('entry.js', `
let result;
try {
  result = OBJECT.SUB.UNKNOWN.a;
} catch (e) {
  result = e.name;
}
console.log('Result:', result);
`);

const compiler = webpack({
  mode: 'none',
  entry: './entry.js',
  output: { filename: 'bundle.js', path: process.cwd() },
  plugins: [
    new webpack.DefinePlugin({
      OBJECT: {
        SUB: {
          KNOWN: JSON.stringify('val')
        }
      }
    })
  ]
});

compiler.run((err, stats) => {
  if (err) throw err;
  console.log('--- Compiled code ---');
  const bundle = fs.readFileSync('bundle.js', 'utf8');
  console.log(bundle.split('\\n').filter(l => l.includes('try {')).join('\\n'));
  
  console.log('--- Execution ---');
  import('./bundle.js');
});

Steps:

  1. Run the script with node.
  2. Inspect the compiled bundle and stdout output.

Expected Behavior

  • OBJECT.SUB.UNKNOWN -> evaluates to undefined.
  • OBJECT.SUB.UNKNOWN?.a -> evaluates to undefined (short-circuits via optional chaining).
  • OBJECT.SUB.UNKNOWN.a -> throws TypeError: Cannot read properties of undefined (reading 'a').
  • OBJECT.SUB.UNKNOWN["a"] -> throws TypeError: Cannot read properties of undefined (reading 'a').
  • OBJECT.SUB.UNKNOWN.a.b -> throws TypeError: Cannot read properties of undefined (reading 'a').
  • OBJECT.SUB.UNKNOWN.a?.b -> throws TypeError: Cannot read properties of undefined (reading 'a').

Actual Behavior

Webpack replaces the entire member expression with (undefined):

try {
  result = undefined;
} catch (e) {
  result = e.name;
}

Output:
Result: undefined (no error thrown).

Non-optional property accesses OBJECT.SUB.UNKNOWN.a, OBJECT.SUB.UNKNOWN["a"], OBJECT.SUB.UNKNOWN.a.b, and OBJECT.SUB.UNKNOWN.a?.b all silently evaluate to undefined instead of throwing TypeError.

Environment

System:
  OS: macOS 15.0
Binaries:
  Node: 22.23.1
Packages:
  webpack: 5.104.1 (commit 15beefc7 on main)

Is this a regression?

No

Additional Context

In lib/DefinePlugin.js (lines 1221–1228):

parser.hooks.expressionMemberChain
	.for(chainRoot)
	.tap(PLUGIN_NAME, (expr, members) => {
		const deps = [key];
		if (findUndefinedMemberAccess(members, deps) === -1) return;
		for (const dep of deps) addValueDependency(dep);
		return toConstantDependency(parser, "undefined")(expr);
	});

Notice that findUndefinedMemberAccess(members, deps) returns the index undefinedIndex of the undefined member. In callMemberChain (lines 1231–1252), dropTrailingMembers is used to cut the chain so that only the member up to undefinedIndex is replaced:

parser.hooks.callMemberChain
	.for(chainRoot)
	.tap(PLUGIN_NAME, (expr, members, membersOptionals) => {
		const deps = [key];
		const undefinedIndex = findUndefinedMemberAccess(members, deps);
		if (undefinedIndex === -1) return;
		for (const dep of deps) addValueDependency(dep);
		if (membersOptionals[undefinedIndex + 1]) {
			return toConstantDependency(parser, "undefined")(expr);
		}
		toConstantDependency(
			parser,
			"undefined"
		)(
			dropTrailingMembers(
				/** @type {Expression} */ (expr.callee),
				members.length - 1 - undefinedIndex
			)
		);
		parser.walkExpressions(expr.arguments);
		return true;
	});

In expressionMemberChain, membersOptionals is also passed by JavascriptParser (this.hooks.expressionMemberChain.call(..., expression, members, membersOptionals, memberRanges)), but DefinePlugin does not check undefinedIndex or membersOptionals[undefinedIndex + 1], replacing the whole expression expr with "undefined".

Activity

  1. added
    area: parserJavaScript parsing and dependency analysis (lib/javascript, lib/dependencies)
    on Sep 8, 2026
  2. Pcmhacker-piro commented on Sep 8, 2026

    @Pcmhacker-piro
    Author

    I have investigated this and have a reproduction test and fix ready. Preparing a pull request with the fix and test cases shortly.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: parserJavaScript parsing and dependency analysis (lib/javascript, lib/dependencies)

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions