Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
like image 867
tkers Avatar asked Sep 28 '15 11:09

tkers


People also ask

What is the syntax of object defineProperty () method in JavaScript?

Syntax: Object. defineProperty(obj, prop, descriptor)

What parameters does the function object defineProperty () accept?

Parameters. The object on which to define the property. The name or Symbol of the property to be defined or modified. The descriptor for the property being defined or modified.

How do you set one object property that Cannot be changed or modified?

The most common and well-known way to keep an object from being changed is by using the const keyword. You can't assign the object to something else, unlike let and var , but you can still change the value of every property, delete a property, or create a property.


2 Answers

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.

like image 92
Bergi Avatar answered Sep 22 '22 15:09

Bergi


Since I noticed you using the arrow function I'm going to assume you have access to ES6, meaning you have access to classes too.

You can just simply extend the Error class. For example:

class CustomError extends Error {
    toJSON() {
        return {
            name: 'CustomError',
            message: this.message
        };
    }
}
like image 25
Ben Fortune Avatar answered Sep 21 '22 15:09

Ben Fortune