Skip to content

assert: fix TypeError on deepStrictEqual with null Map key or Set member - #64449

Open
semx wants to merge 1 commit into
nodejs:mainfrom
semx:fix-deepstrictequal-map-null-key
Open

assert: fix TypeError on deepStrictEqual with null Map key or Set member#64449
semx wants to merge 1 commit into
nodejs:mainfrom
semx:fix-deepstrictequal-map-null-key

Conversation

@semx

@semx semx commented Jul 12, 2026

Copy link
Copy Markdown

assert.deepStrictEqual() (and util.isDeepStrictEqual()) throw a TypeError instead of comparing when the first Map has a null (or other primitive) key that lines up against object-only keys in the other map:

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

const a = new Map([[null, 1], [{}, 2]]);
const b = new Map([[{}, 9], [{}, 9]]);

assert.deepStrictEqual(a, b);
// TypeError: Cannot read properties of null (reading 'constructor')
// expected: an AssertionError (the maps are not deeply equal)

The same happens for an undefined key. Set has the identical problem for a null/undefined member (once the set is large enough to skip the small-set fast path):

assert.deepStrictEqual(new Set([null, {}, {}]), new Set([{}, {}, {}]));
// TypeError: Cannot read properties of null (reading 'constructor')

It only triggers in strict mode when the other collection's keys/members are all objects and their count equals the first collection's size.

Cause

In mapObjectEquiv and setObjectEquiv (lib/internal/util/comparisons.js), primitive/null keys and members are resolved directly via b.has() / b.get(), but that handling was gated behind extraChecks (array.length !== a.size). When the counts match, the gate is skipped and the primitive/null key/member falls through to objectComparisonStart, which dereferences .constructor and throws on null/undefined.

Fix

Handle primitive/null keys and members unconditionally — they can only match by identity and can never match through the object comparator — so they are always resolved by direct lookup and never reach objectComparisonStart. The collections above now compare as unequal (throwing an AssertionError, as expected) instead of throwing a TypeError. Object comparison is unchanged.

Added regression cases (null and undefined keys/members, for both Map and Set) to test/parallel/test-assert-deep.js.

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. util Issues and PRs related to the built-in util module. labels Jul 12, 2026
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.19%. Comparing base (718cfe4) to head (d28dd4e).
⚠️ Report is 12 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64449      +/-   ##
==========================================
- Coverage   92.37%   90.19%   -2.18%     
==========================================
  Files         417      771     +354     
  Lines      188656   264911   +76255     
  Branches    28838    50318   +21480     
==========================================
+ Hits       174268   238937   +64669     
- Misses      14051    16925    +2874     
- Partials      337     9049    +8712     
Files with missing lines Coverage Δ
lib/internal/util/comparisons.js 99.53% <100.00%> (+<0.01%) ⬆️

... and 483 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.

@semx
semx force-pushed the fix-deepstrictequal-map-null-key branch from 19f16d9 to 98843e4 Compare July 15, 2026 22:00
@semx

semx commented Jul 30, 2026

Copy link
Copy Markdown
Author

This has two approvals (thanks @ljharb, @jasnell) and is still labelled needs-ci. Could a collaborator kick off a CI run?

Happy to rebase first if that helps — the branch is otherwise unchanged since the reviews.

@semx

semx commented Aug 5, 2026

Copy link
Copy Markdown
Author

Following up: this has two approvals (thanks @ljharb, @jasnell) and is only waiting on CI to run so it can land. Could a collaborator start CI / apply the label so it can move through the commit-queue? Happy to rebase if needed. Thanks!

@ljharb

ljharb commented Aug 6, 2026

Copy link
Copy Markdown
Member

First I think it needs a rebase?

@semx
semx force-pushed the fix-deepstrictequal-map-null-key branch from 98843e4 to cdd7206 Compare August 6, 2026 10:03
@semx

semx commented Aug 6, 2026

Copy link
Copy Markdown
Author

Rebased onto current main and force-pushed. There was one conflict, in lib/internal/util/comparisons.js: main has since landed an equivalent fix for the Map path (mapObjectEquiv), so I dropped that now-redundant hunk and kept the still-needed Set-path fix (setObjectEquiv) plus all four null/undefined regression tests. The final diff is just the Set fix + tests; the branch is one commit on top of main and both files pass node --check. Should be good for CI now — thanks!

deepStrictEqual() and util.isDeepStrictEqual() threw "Cannot read
properties of null (reading 'constructor')" instead of comparing when
a Map key or Set member was null/undefined (or another primitive) and
lined up against object-only keys/members in the other collection with
an equal count. The primitive/null handling was gated behind an
optimization that is skipped when the counts match, letting such keys
reach objectComparisonStart, which dereferences `.constructor`.

Resolve primitive and null keys/members directly in every case.

Signed-off-by: semx <7532921+semx@users.noreply.github.com>
@semx
semx force-pushed the fix-deepstrictequal-map-null-key branch from cdd7206 to d28dd4e Compare September 7, 2026 12:00
@semx

semx commented Sep 7, 2026

Copy link
Copy Markdown
Author

Rebased onto current main — conflict-free (the earlier Map-path hunk stays dropped since mapObjectEquiv landed; this is just the Set-path fix plus the null/undefined regression tests). The PR is mergeable and only needs-ci; the fresh workflow runs are sitting in action_required, so they need a collaborator to approve the run for this fork PR. If someone could approve the Actions run and start CI, it should be ready to move through the commit-queue. Thanks again @ljharb @jasnell for the reviews.

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

Labels

needs-ci PRs that need a full CI run. util Issues and PRs related to the built-in util module.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants