skip to Main Content

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


  1. 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?

    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.

    const proto = {foo: 1};
    const obj = Object.create(proto);  // {} -> {foo: 1} chain
    
    console.log(obj.foo, Object.hasOwn(obj, 'foo'));  // 1 false
    
    obj.foo = 2;
    console.log(obj.foo, Object.hasOwn(obj, 'foo'));  // 2 true
    console.log(Object.getPrototypeOf(obj).foo);  // 1, prototype unchanged
    console.log(Object.getPrototypeOf(obj) === proto);  // true, no copy
    
    delete obj.foo;
    console.log(obj.foo, Object.hasOwn(obj, 'foo'));  // 1 false
    Login or Signup to reply.
  2. 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 to Error.

    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 like Promise that are forced to use new.

    Error.prototype isn’t supposed to be accessed under normal circumstances. It was needed to implement custom error classes in ES5. But Error 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 for error.message:

    let e = Error('error');
    console.log(e.message); // 'error'
    e.message = undefined;
    console.log(e.message); // undefined
    delete e.message;
    console.log(e.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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search