Summary
__proto__ returns something that does not compare equal to the object's actual prototype, for plain objects and class instances. Object.getPrototypeOf is correct on the same objects, so the two disagree. Arrays are unaffected. Silent wrong answer, no error.
const o = { a: 1 };
const sink = []; sink.push(o); // fully escaped, real heap object
o.__proto__ === Object.prototype // perry: false node: true
Object.getPrototypeOf(o) === Object.prototype // perry: true node: true ✓
const a = [1]; sink.push(a);
a.__proto__ === Array.prototype // perry: true node: true ✓
Object.getPrototypeOf(a) === Array.prototype // perry: true node: true ✓
class C {} const c = new C(); sink.push(c);
c.__proto__ === C.prototype // perry: false node: true
Full output:
perry: false true / true true / false
node: true true / true true / true
The receivers are pushed into a live array first, so they are genuinely escaped heap objects — this is not the scalar-replacement folding of #10689, and it reproduces on a tree without that fix.
Shape of the divergence
| receiver |
__proto__ |
Object.getPrototypeOf |
| plain object literal |
wrong |
correct |
| array |
correct |
correct |
| class instance |
wrong |
correct |
That Object.getPrototypeOf is right while __proto__ is wrong on the same object means the prototype link itself is intact and the __proto__ accessor is returning a different value — a distinct object, or a re-derived intrinsic that is not the same identity as the one Object.prototype names. Arrays working suggests the array path resolves the intrinsic differently from the object and class paths.
Why it matters
__proto__ is deprecated but very widely used in existing library code for prototype identity checks — plain-object detection, clone and merge helpers, and x.__proto__ === Y.prototype tests in general. Those take the wrong branch silently rather than throwing, which is the hard failure mode to attribute from downstream.
It also means the two documented ways of asking the same question give different answers, so a reader cannot trust either without checking both.
Environment
perry built from main 68a545439 + #10611, Linux x86-64, compared against node v26.8.1. Found while fixing #10689; independent of it.
Summary
__proto__returns something that does not compare equal to the object's actual prototype, for plain objects and class instances.Object.getPrototypeOfis correct on the same objects, so the two disagree. Arrays are unaffected. Silent wrong answer, no error.Full output:
The receivers are pushed into a live array first, so they are genuinely escaped heap objects — this is not the scalar-replacement folding of #10689, and it reproduces on a tree without that fix.
Shape of the divergence
__proto__Object.getPrototypeOfThat
Object.getPrototypeOfis right while__proto__is wrong on the same object means the prototype link itself is intact and the__proto__accessor is returning a different value — a distinct object, or a re-derived intrinsic that is not the same identity as the oneObject.prototypenames. Arrays working suggests the array path resolves the intrinsic differently from the object and class paths.Why it matters
__proto__is deprecated but very widely used in existing library code for prototype identity checks — plain-object detection, clone and merge helpers, andx.__proto__ === Y.prototypetests in general. Those take the wrong branch silently rather than throwing, which is the hard failure mode to attribute from downstream.It also means the two documented ways of asking the same question give different answers, so a reader cannot trust either without checking both.
Environment
perry built from main
68a545439+ #10611, Linux x86-64, compared against node v26.8.1. Found while fixing #10689; independent of it.