Search code examples
javascriptnode.jspropertiesecmascript-6prototype

Object.defineProperty to override read-only property


In our NodeJS application, we define custom error classes by extending the default Error object:

"use strict";
const util = require("util");

function CustomError(message) {
    Error.call(this);
    Error.captureStackTrace(this, CustomError);
    this.message = message;
}

util.inherits(CustomError, Error);

This allows us to throw CustomError("Something"); with the stack trace showing up correctly, and both instanceof Error and instanceof CustomError working correctly.

However, for returning errors in our API (over HTTP), we want to convert the error to JSON. Calling JSON.stringify() on an Error results in "{}", which is obviously not really descriptive for the consumer.

To fix this, I thought of overriding CustomError.prototype.toJSON(), to return an object literal with the error name and message. JSON.stringify() would then just stringify this object and all would work great:

// after util.inherits call

CustomError.prototype.toJSON = () => ({
    name    : "CustomError",
    message : this.message
});

However, I quickly saw that this throws a TypeError: Cannot assign to read only property 'toJSON' of Error. Which might make sense as I'm trying to write to the prototype. So I changed the constructor instead:

function CustomError(message) {
    Error.call(this);
    Error.captureStackTrace(this, CustomError);
    this.message = message;
    this.toJSON = () => ({
        name    : "CustomError",
        message : this.message
    });
}

This way (I expected), the CustomError.toJSON function would be used and the CustomError.prototype.toJSON (from Error) would be ignored.

Unfortunately, this just throws the error upon object construction: Cannot assign to read only property 'toJSON' of CustomError.

Next I tried removing "use strict"; from the file, which sort of solved the problem in that no error was being thrown anymore, although the toJSON() function was not used by JSON.stringify() at all.

At this point I'm just desperate and just try random things. Eventually I end up with using Object.defineProperty() instead of directly assigning to this.toJSON:

function CustomError(message) {
    Error.call(this);
    Error.captureStackTrace(this, CustomError);
    this.message = message;
    Object.defineProperty(this, "toJSON", {
        value: () => ({
            name    : "CustomError",
            message : this.message
        })
    });

This works perfectly. In strict mode, no errors are being called, and JSON.stringify() returns {"name:" CustomError", "message": "Something"} like I want it to.

So although it works as I want it to now, I still want to know:

  1. Why does this work exactly? I expect it to be the equivalent to this.toJSON = ... but apparently it is not.
  2. Should it work like this? I.e. is it safe to depend on this behaviour?
  3. If not, how should I override the toJSON method correctly? (if possible at all)

Solution

  • Why does this work exactly?

    Object.defineProperty just defines a property (or alters its attributes, if it already exists and is configurable). Unlike an assignment this.toJSON = … it does not care about any inheritance and does not check whether there is an inherited property that might be a setter or non-writable.

    Should it work like this? I.e. is it safe to depend on this behaviour?

    Yes, and yes. Probably you can even use it on the prototype.


    For your actual use case, given a recent node.js version, use an ES6 class that extends Error for the best results.