You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Update assert.deepStrictEqual docs to mention .constructor property #60239
I’ve been running into an bug in my code where assert.deepStrictEqual(a, b) was throwing unexpectedly.
To make a long story short, a and b were instances of a derived class that returned a new Proxy object in its constructor. The handler for this proxy set a new Function.prototype.bind call on any function object from the target, but this accidentally caught the target.constructor property in the line of fire — the intent was to only rebind instance methods. Basically, a and b shared the same prototype, but had unequal .constructor properties due to the incorrect binding.
The docs for assert.deepStrictEqual say that only the [[Prototype]] and enumerable own properties are checked, so I was scratching my head trying to track down the bug, meticulously checking my code against the docs point by point. Only by stepping through the Node.JS source code did I see where .constructor is accessed. Then I found the new docs for util.isDeepStrictEqual, which in v24 has been updated to mention explicitly that constructors are checked. This would’ve been good to know!
So this issue is a request to update the assert.deepStrictEqual docs by adding a bullet point stating how object constructors are included in the comparison algorithm. Thank you!
@BridgeAR Sorry if I wasn’t clear, I didn’t mean to imply that any code was passing before and now it breaks. When I said “throwing unexpectedly” I meant I expected it to pass but it doesn’t. TLDR first, then details below.
<TLDR>
constunderlying=newType();consta=newNominalType(underlying);constb=newNominalType(underlying);// check prototypes:assert.strictEqual(Reflect.getPrototypeOf(a),Reflect.getPrototypeOf(b));// passes// check enumerable own properties:for(constkeyofReflect.ownKeys(a)){assert.deepStrictEqual(a[key],b[key]);// passes}// but!assert.deepStrictEqual(a,b);// fails! but why?// oh, right, forgot to check constructors: (not in the docs)assert.strictEqual(a.constructor,b.constructor);// fails
Updating the docs to explicitly say that constructors are checked in the deepStrictEqual algorithm would be very helpful.
</TLDR>
How to end up in a situation like this: A subclass returns a Proxy in its constructor that calls Function.prototype.bind on all function properties of the target, unintentionally including the .constructor property.
/** Represents a type in a type system. */classType{constructor(){...}intersect(type){...}union(type){...}isSubtypeOf(type){...}}/** A nominal type *is* a type but also *has* an underlying type, which serves as its structure. */classNominalTypeextendsType{/** @type {Type} */underlyingType;constructor(underlyingType){super();this.underlyingType=underlyingType;returnnewProxy(this,{get(target,key,receiver){// 1. Check if this class directly has itif(Object.hasOwn(target,key)||Object.hasOwn(Reflect.getPrototypeOf(target),key)){returnReflect.get(target,key,receiver);}// 2. Fallback to underlying typeconstvalue=target.underlyingType[key];// bind all instance methods to the underlying typeif(typeofvalue==='function'){// <-- here’s the bug… see explanation belowreturnvalue.bind(target.underlyingType);}returnvalue;},});}/** @override --- different subtyping rules for nominal types. */isSubtypeOf(type){...}}
The bug is in the line if (typeof value === 'function'). If value is the constructor function, it gets rebound to target.underlyingType, which is not what we want since Function.prototype.bind returns a new function. That’s how we’re able to end up with two objects that share the same prototype and have deep-equal enumerable own properties, but have unequal constructors.
At first I forgot to check the constructors, and couldn’t figure out why assert.deepStrictEqual(a, b) fails. Updating the docs to explicitly say that constructors are checked in the deepStrictEqual algorithm would be very helpful.
BTW, the bug was fixed by updating the if statement’s condition.
-if (typeof value === 'function') {+if (typeof value === 'function' && key !== 'constructor') {
This issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.
Activity
BridgeAR commentedon Oct 17, 2025
@chharvey could you please provide a small sample code that fails that passed before? There was no intentional breaking change with recent changes.
chharvey commentedon Oct 17, 2025
@BridgeAR Sorry if I wasn’t clear, I didn’t mean to imply that any code was passing before and now it breaks. When I said “throwing unexpectedly” I meant I expected it to pass but it doesn’t. TLDR first, then details below.
<TLDR>
Updating the docs to explicitly say that constructors are checked in the
deepStrictEqualalgorithm would be very helpful.</TLDR>
How to end up in a situation like this: A subclass returns a
Proxyin its constructor that callsFunction.prototype.bindon all function properties of the target, unintentionally including the.constructorproperty.The bug is in the line
if (typeof value === 'function'). Ifvalueis the constructor function, it gets rebound totarget.underlyingType, which is not what we want sinceFunction.prototype.bindreturns a new function. That’s how we’re able to end up with two objects that share the same prototype and have deep-equal enumerable own properties, but have unequal constructors.At first I forgot to check the constructors, and couldn’t figure out why
assert.deepStrictEqual(a, b)fails. Updating the docs to explicitly say that constructors are checked in thedeepStrictEqualalgorithm would be very helpful.BTW, the bug was fixed by updating the
ifstatement’s condition.With this fix,
BridgeAR commentedon Oct 21, 2025
Thank you, that makes sense.
github-actions commentedon May 20, 2026
This issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.
chharvey commentedon May 20, 2026
this will be closed by #60253 and #62010 once merged.