Skip to content

errors: validate constructor name - #65607

Open
christianaurichzm wants to merge 1 commit into
nodejs:mainfrom
christianaurichzm:errors-validate-constructor-name
Open

errors: validate constructor name#65607
christianaurichzm wants to merge 1 commit into
nodejs:mainfrom
christianaurichzm:errors-validate-constructor-name

Conversation

@christianaurichzm

Copy link
Copy Markdown
Contributor

determineSpecificType() decides whether an error message can name the constructor of a value. The check is
value.constructor && 'name' in value.constructor: the in operator requires an object on its right-hand side, and an empty name passes it.

On v22.23.1, and the same on main:

const fs = require('node:fs');

fs.readFileSync(JSON.parse('{"constructor": 5}'));
// TypeError: Cannot use 'in' operator to search for 'name' in 5      <- no code

fs.readFileSync(new (class {})());
// TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string
// or an instance of Buffer or URL. Received an instance of

The first one is the one that matters. constructor is an ordinary JSON key, so untrusted input can make the construction of the message throw: the ERR_INVALID_ARG_TYPE never reaches the caller, it is replaced by a plain TypeError with no code, and err.code === 'ERR_INVALID_ARG_TYPE' at the call site stops matching. The second is cosmetic: an anonymous class owns a name that is the empty string, so the message ends in a dangling Received an instance of .

Representative cases:

value before after
new (class {})() an instance of {}
JSON.parse('{"constructor": 5}') throws TypeError [Object]
{ constructor: { name: Symbol('x') } } throws TypeError [Object]
{ constructor: { name: 42 } } an instance of 42 [Object]

This reads constructor and its name once, and uses the name only when it is a non-empty string. The single read is also a change in behaviour: the current check reads constructor three times, which an accessor can observe. Tests pin
both reads.

test/common's invalidArgTypeHelper moves to the same check. #49696 updated it to match its deliberate change to the function branch, but left the object branch on the original truthy check, so the two have disagreed since.

Scope

Requiring a string intentionally also changes non-string constructor names. { constructor: { name: 42 } } produces a well-formed an instance of 42 today and now falls back to [Object], the same fallback already used when no name is available.

The function branch is unchanged. Its handling of anonymous function names was changed deliberately in #49696.

Testing

  • test/parallel/test-error-value-type-detection.mjs: extended with the cases in the table, assertions that constructor and its name are each read once, and a check that building the error keeps its code
  • benchmark/error/determine-specific-type.js, baseline versus patch on Linux x64, three comparisons, the last with --runs 60 --analyze: the configurations flagged at the 5% threshold were not consistent between runs. compare.js reports 1.70 expected false positives at that threshold across its 34 configurations
  • make -j16 test and make lint: pass

Refs: #49696

determineSpecificType() checks for a usable constructor name with
`'name' in value.constructor`. That accepts an empty name, and the `in`
operator requires its right-hand side to be an object.

Anonymous classes own a `name` that is the empty string, so they produce
messages ending in a dangling "Received an instance of ". A truthy
primitive `constructor` reaches the `in` operator and throws while the
message is being built, so ERR_INVALID_ARG_TYPE is replaced by a
TypeError carrying no `code`. The latter is reachable from untrusted
input, since `constructor` is an ordinary JSON key.

The check changed in nodejs#49696, while this function was rewritten as a
switch. That pull request updated test/common's invalidArgTypeHelper to
match its deliberate change to the `function` branch, but left the
helper's `object` branch on the original truthy check, so the two have
disagreed since.

Read `constructor` and its `name` once and use the name only when it is
a non-empty string. Requiring a string also stops meaningless names from
being interpolated: 42 currently yields "an instance of 42", and a
symbol name throws outright. The current check reads `constructor` three
times, which an accessor can observe.

Move the helper to the same check so the two cannot drift apart again.

Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com>
@nodejs-github-bot nodejs-github-bot added errors Issues and PRs related to JavaScript errors originated in Node.js core. needs-ci PRs that need a full CI run. labels Aug 28, 2026
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.05%. Comparing base (9baabd4) to head (78e354f).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65607      +/-   ##
==========================================
- Coverage   90.07%   90.05%   -0.03%     
==========================================
  Files         751      751              
  Lines      254875   254921      +46     
  Branches    48108    48124      +16     
==========================================
- Hits       229579   229566      -13     
- Misses      16466    16530      +64     
+ Partials     8830     8825       -5     
Files with missing lines Coverage Δ
lib/internal/errors.js 98.46% <100.00%> (+<0.01%) ⬆️

... and 44 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

errors Issues and PRs related to JavaScript errors originated in Node.js core. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants