Repository navigation
DefinePlugin: non-optional property access past an undefined member incorrectly evaluates to undefined instead of throwing TypeError #22014
Description
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:
- Run the script with
node. - Inspect the compiled bundle and stdout output.
Expected Behavior
OBJECT.SUB.UNKNOWN-> evaluates toundefined.OBJECT.SUB.UNKNOWN?.a-> evaluates toundefined(short-circuits via optional chaining).OBJECT.SUB.UNKNOWN.a-> throwsTypeError: Cannot read properties of undefined (reading 'a').OBJECT.SUB.UNKNOWN["a"]-> throwsTypeError: Cannot read properties of undefined (reading 'a').OBJECT.SUB.UNKNOWN.a.b-> throwsTypeError: Cannot read properties of undefined (reading 'a').OBJECT.SUB.UNKNOWN.a?.b-> throwsTypeError: 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".
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
Pcmhacker-piro commentedon Sep 8, 2026
I have investigated this and have a reproduction test and fix ready. Preparing a pull request with the fix and test cases shortly.
fix(define): throw on a non-optional read past an undefined member
fix(define): throw on a non-optional read past an undefined member (w…