Today, during some testing, I was interested in seeing all properties of an Error object.
I wrote this Method to get all available properties, not caring about ‘hasOwnProperty’.
/**
* Get all properties of an object and don't care about enumerable or prototyp.
*
* @internal
* @param {object} obj
* @return {[string, unknown][]}
*/
export function getAllProperties(obj: object): [string, unknown][] {
const properties: [string, unknown][] = [];
const prototypChain: string[] = [];
let currentObj = obj;
while (currentObj !== Object.prototype && currentObj !== null) {
Object.getOwnPropertyNames(currentObj).forEach((key) => {
if (prototypChain.length > 0) {
// @ts-expect-error I want it this way!
properties.push([`${prototypChain.join('.')}.${key}`, currentObj[key]]);
} else {
// @ts-expect-error I want it this way!
properties.push([key, currentObj[key]]);
}
});
currentObj = Object.getPrototypeOf(currentObj);
prototypChain.push('prototyp');
}
return properties;
}
Imagine my surprise when I used it to look at an error thrown with new Error('...')
.
{
stack: "Error: This is a test error
at Object.<anonymous> (/Users/.../Entwicklung/Bit/bit-log/src/appender/__tests__/FileAppender.spec.ts:256:13)",
message: "This is a test error",
prototyp.constructor: [Function function Error() { [native code] }],
prototyp.name: "Error",
prototyp.message: "",
prototyp.toString: [Function function toString() { [native code] }]
}
How is it possible to have an object with inheritance that does not correctly overwrite a property in the parent?
Or is my expectation wrong that there should not be two different message properties?
After all, this is a standard object and not a custom implementation.
2
Answers
It sounds like your misconception here is that an object with a prototype chain has a copy of a set of properties associated with each prototype, and that an assignment will look up and modify the corresponding one. That’s not how it works; creating an object using a prototype does not copy the prototype, and the default effect of assigning new values to properties is to create those properties on the end object, shadowing any prototype ones with the same name.
The way
Error
instance behaves in this example is expected for prototypal inheritance, this needs to be taken into account, another answer addresses this. But this not representative in general and is specific toError
.Error
is odd for historical reasons, as well as some other legacy native constructor functions. They can be identified by their allowed usage,Error()
. This is different for classes likePromise
that are forced to usenew
.Error.prototype
isn’t supposed to be accessed under normal circumstances. It was needed to implement custom error classes in ES5. ButError
is extensible since ES6,class extends Error
is treated in a special way by JavaScript engines to make it work smoothly.Error.prototype.message
is one of these quirks. As noted in the comments, prototypal inheritance is used to make it a default value forerror.message
:It wouldn’t be implemented this way in modern JavaScript. In the semantics of ES6 classes, a prototype object holds class methods and accessor properties, not values.