1

The below example of how to create a custom Error in JS can be found on MDN (link).

I am struggling to understand what is going on (specific questions below).

function CustomError(foo, message, fileName, lineNumber) {
  var instance = new Error(message, fileName, lineNumber);
  instance.foo = foo;
  Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
  if (Error.captureStackTrace) {
    Error.captureStackTrace(instance, CustomError);
  }
  return instance;
}

CustomError.prototype = Object.create(Error.prototype, {
  constructor: {
    value: Error,
    enumerable: false,
    writable: true,
    configurable: true
  }
});

if (Object.setPrototypeOf) {
  Object.setPrototypeOf(CustomError, Error);
} else {
  CustomError.__proto__ = Error;
}


try {
  throw new CustomError('baz', 'bazMessage');
} catch (e) {
  console.log(e.foo); //baz
  console.log(e.message); //bazMessage
}

QUESTIONS

  1. Since we are returning an object inside CustomError, will using it as a constructor function (new CustomError()) and using it as a normal function object (CustomError()) yield the same outcome?
  2. In line 11: Do we create a new object here, instead of setting CustomError.prototype directly to Error.prototype, so that we can extend the prototype without affecting all other Error objects?
  3. Also in line 11: Why do we even bother setting prototype property of the function, if we cannot use it as a constructor function (ref. question 1)?
  4. In line 4 we set the Error instance to whatever called the function, right? I don't understand what the purpose is / what the this value will be.
  5. What is the purpose of the captureStackTrace check?

Thank you for helping me analyze and understand this snippet.


EDIT:

I wanted to add that I think I have understood the following:

  • Whenever we create a new object with a constructor function (new keyword), it gets prototype-linked to an empty object, which in turn is prototype linked to Object.prototype
  • It is prototype linked to a new empty object, instead of directly to Object.prototype, because that way we can extend the prototype of the new object, without changing the behavior of all objects with Object.prototype on its prototype chain.
  • If we have two levels of "inheritance", and thus manually change the prototype property of a constructor function, it should reflect the same behavior. In effect, we should set the prototype property to be an empty object, which in turn is prototype linked to our newly introduced "parent"

Example:

function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}

function Male(name) {
  Person.call(this, name, "male");
}

Male.prototype = Object.create(Person.prototype, {
  constructor: {
    value: Male,
    enumerable: false,
    writeable: true
  }
});

var person1 = new Male("Chris");
  • As seen above, when changing the prototype property manually, we should not only assign a new empty object to the prototype property, but also set the constructor property of that empty object
  • This is because every object should indeed be able to look at its prototype to figure out what object constructed it. This follows the behavior of Object.prototype, where the constructor property is Object (same with other built-ins)

That should explain the second block. Have I understood that part correctly?

Magnus
  • 6,791
  • 8
  • 53
  • 84

2 Answers2

1
  1. When using CustomError as normal function the value of this is undefined inside it. But if it is used as constructor function this will refer to current instance. Considering this is passed to getPrototypeOf, calling it as normal function will throw error and break the code. But it would be the same if CustomError was imlemented like below:

    function CustomError(){
        if(!(this instanceof MyError)) return new CustomError();
    }
    
  2. That's exactly like you think

  3. I am not sure why you think it can't be used as constructor function because it has already been called with new keyword in try/catch statement

  4. In line 4 instance's prototype is set to the prototype of this. this refers to current instance of CustomError since it's been called via new.

  5. Since Error.captureStackTrace is not supported by all environments (afaik only chrome and nodejs supports it), it checks if the function exists before using it.


Edit part

  • Whenever we create a new object with a constructor function (new keyword), it gets prototype-linked to Person.prototype (at least has consctructor property), which in turn is prototype linked to Object.prototype

  • It is prototype linked to Person.prototype, because Person constructor created it and constructors link newly created objects to their own prototype objects. Yes you can change Person.prototype without affecting Object.prototype.

Hikmat G.
  • 2,591
  • 1
  • 20
  • 40
  • Thanks Hikmat, I will spend some time digesting what you wrote, then follow up with any questions. Regarding your "edit part", yep I totally follow you. I just wanted to highlight the additional fact that constructor functions `Male` and `Female` might both use `Person.prototype` as their *prototypal object*, but because we set `Male.prototype` and `Female.prototype` to an empty object prototype linked to `Person.prototype` (with `Create.object()`), instead of setting it directly to `Person.prototype`, we ensure that we can change the prototype for `Male`, without affecting `Female`. – Magnus Jul 09 '18 at 02:46
  • I thought that if we return an object in a constructor function, any use of `this` to refer to a newly created object is ignored (i.e. the constructor function will work just like a normal function). Is that the wrong understanding? – Magnus Jul 09 '18 at 02:50
  • If you call a function with `new` kewword, that function has access to `this` regardless of what is going on inside function. – Hikmat G. Jul 09 '18 at 06:36
  • Hikmat, I have been researching this a bit, and it seems that if we return an object from a constructor function, it no longer works the way you would expect a constructor function to work. Instead of assigning `this` to the new object, then returning it, it will just return the object from the `return` statement, like any other function. See: https://www.bennadel.com/blog/2522-providing-a-return-value-in-a-javascript-constructor.htm, and: https://stackoverflow.com/questions/1978049/what-values-can-a-constructor-return-to-avoid-returning-this – Magnus Jul 09 '18 at 22:43
  • This as well: https://web.archive.org/web/20100216124827/http://www.gibdon.com/2008/08/javascript-constructor-return-value.html – Magnus Jul 09 '18 at 22:43
  • You are absolutely right about what will be the value of `new A()` e.g. But what I am saying is completely different. Check https://stackblitz.com/edit/js-8gknrd. As you can see the function returns an array but still has access to 'real' `this` before returning which is the case in `CustomError`; – Hikmat G. Jul 10 '18 at 06:01
  • Hikmat, sorry for the massive delay, will close this one off as soon as possible. Thanks for that live snippet, I see that the function still has access to `this` as if it was a new object. That leads me to a follow-up question: Since it is already prototype linked to `Error`, why not just return `this` (i.e. no return statement) the normal constructor way, and thus avoid creating a new `Error` `instance` inside `CustomError`? – Magnus Jul 17 '18 at 09:28
  • I am not sure on this, but I think the reason is because it's shorter to use `Error` constructor to create an object that has non-enumerable `message`, `fileName`, `lineNumber` properties than defining same named properties with same configuration on `this` – Hikmat G. Jul 17 '18 at 16:53
1

Error() is a beast. When its constructor is called, it simply ignores this and creates a new Error object. Normally, one would use Error.call(this,...) to have the new subclass instance initialized for its superclass bofore initializing its own modifications. However, with Error, this will fail, as you'll get an initialized new Error object returned and the actual instance remains unaltered. Error() simply ignores its this and always creates a new instance. To circumvent this, a new Error has to be created, then its prototype needs to be changed to the custom prototype (derived from Error.prototype) and this instance is returned, throwing away the instance that was created by the initial new, like Error does (so we have to copy this bad behavior of Error throughout all subclasses).

The following works:

MyErrorr = function() { // Class MyError
    var prototype = Object.create (Error.prototype, { name: {writable: false, enumerable: true, value: "MyError"}});
    function MyError (reason, callee) {
        if(reason instanceof MyError)
            return new MyError(reason.message); // clone
        // creating an Error is a bit tricky, as Error.call(this, message) behaves like new Error(message), creating a new instance
        var instance = new Error(reason);
        if(Error.captureStackTrace)
            Error.captureStackTrace(instance, callee?callee:MyError); // exclude the callee or at least MyError() from the stack trace
        var stack = instance.stack; // force creation of the stack output before the prototype is switched (else the formatting is different, no idea why)
        if(Object.setPrototypeOf)
            Object.setPrototypeOf(instance, prototype);
        else
            instance.__proto__ = prototype;
        return instance;
    };
    prototype.constructor = ConversionError;
    MyError.prototype = prototype;
    return MyError;
}();

Since 'this' is not used, MyError can be instantiated with and without using new (just like Error)

There's a problem though: All those existent Error types are actually instances of the same Error class, not subclasses of Error. At least on Chrome. Also, some functions are part of Error and not of Error.prototype, so they are not available in your class (like captureStackTrace). Maybe it works if you clone them.