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:
this.toJSON = ...
but apparently it is not.Syntax: Object. defineProperty(obj, prop, descriptor)
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.
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.
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.
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
};
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With