admin管理员组

文章数量:1126353

Reproducing the problem

I'm running into an issue when trying to pass error messages around using web sockets. I can replicate the issue I am facing using JSON.stringify to cater to a wider audience:

// node v0.10.15
> var error = new Error('simple error message');
    undefined

> error
    [Error: simple error message]

> Object.getOwnPropertyNames(error);
    [ 'stack', 'arguments', 'type', 'message' ]

> JSON.stringify(error);
    '{}'

The problem is that I end up with an empty object.

What I've tried

Browsers

I first tried leaving node.js and running it in various browsers. Chrome version 28 gives me the same result, and interestingly enough, Firefox at least makes an attempt but left out the message:

>>> JSON.stringify(error); // Firebug, Firefox 23
{"fileName":"debug eval code","lineNumber":1,"stack":"@debug eval code:1\n"}

Replacer function

I then looked at the Error.prototype. It shows that the prototype contains methods such as toString and toSource. Knowing that functions can't be stringified, I included a replacer function when calling JSON.stringify to remove all functions, but then realized that it too had some weird behavior:

var error = new Error('simple error message');
JSON.stringify(error, function(key, value) {
    console.log(key === ''); // true (?)
    console.log(value === error); // true (?)
});

It doesn't seem to loop over the object as it normally would, and therefore I can't check if the key is a function and ignore it.

The Question

Is there any way to stringify native Error messages with JSON.stringify? If not, why does this behavior occur?

Methods of getting around this

  • Stick with simple string-based error messages, or create personal error objects and don't rely on the native Error object.
  • Pull properties: JSON.stringify({ message: error.message, stack: error.stack })

Updates

@Ray Toal Suggested in a comment that I take a look at the property descriptors. It is clear now why it does not work:

var error = new Error('simple error message');
var propertyNames = Object.getOwnPropertyNames(error);
var descriptor;
for (var property, i = 0, len = propertyNames.length; i < len; ++i) {
    property = propertyNames[i];
    descriptor = Object.getOwnPropertyDescriptor(error, property);
    console.log(property, descriptor);
}

Output:

stack { get: [Function],
  set: [Function],
  enumerable: false,
  configurable: true }
arguments { value: undefined,
  writable: true,
  enumerable: false,
  configurable: true }
type { value: undefined,
  writable: true,
  enumerable: false,
  configurable: true }
message { value: 'simple error message',
  writable: true,
  enumerable: false,
  configurable: true }

Key: enumerable: false.

Accepted answer provides a workaround for this problem.

Reproducing the problem

I'm running into an issue when trying to pass error messages around using web sockets. I can replicate the issue I am facing using JSON.stringify to cater to a wider audience:

// node v0.10.15
> var error = new Error('simple error message');
    undefined

> error
    [Error: simple error message]

> Object.getOwnPropertyNames(error);
    [ 'stack', 'arguments', 'type', 'message' ]

> JSON.stringify(error);
    '{}'

The problem is that I end up with an empty object.

What I've tried

Browsers

I first tried leaving node.js and running it in various browsers. Chrome version 28 gives me the same result, and interestingly enough, Firefox at least makes an attempt but left out the message:

>>> JSON.stringify(error); // Firebug, Firefox 23
{"fileName":"debug eval code","lineNumber":1,"stack":"@debug eval code:1\n"}

Replacer function

I then looked at the Error.prototype. It shows that the prototype contains methods such as toString and toSource. Knowing that functions can't be stringified, I included a replacer function when calling JSON.stringify to remove all functions, but then realized that it too had some weird behavior:

var error = new Error('simple error message');
JSON.stringify(error, function(key, value) {
    console.log(key === ''); // true (?)
    console.log(value === error); // true (?)
});

It doesn't seem to loop over the object as it normally would, and therefore I can't check if the key is a function and ignore it.

The Question

Is there any way to stringify native Error messages with JSON.stringify? If not, why does this behavior occur?

Methods of getting around this

  • Stick with simple string-based error messages, or create personal error objects and don't rely on the native Error object.
  • Pull properties: JSON.stringify({ message: error.message, stack: error.stack })

Updates

@Ray Toal Suggested in a comment that I take a look at the property descriptors. It is clear now why it does not work:

var error = new Error('simple error message');
var propertyNames = Object.getOwnPropertyNames(error);
var descriptor;
for (var property, i = 0, len = propertyNames.length; i < len; ++i) {
    property = propertyNames[i];
    descriptor = Object.getOwnPropertyDescriptor(error, property);
    console.log(property, descriptor);
}

Output:

stack { get: [Function],
  set: [Function],
  enumerable: false,
  configurable: true }
arguments { value: undefined,
  writable: true,
  enumerable: false,
  configurable: true }
type { value: undefined,
  writable: true,
  enumerable: false,
  configurable: true }
message { value: 'simple error message',
  writable: true,
  enumerable: false,
  configurable: true }

Key: enumerable: false.

Accepted answer provides a workaround for this problem.

Share Improve this question edited May 23, 2017 at 10:31 CommunityBot 11 silver badge asked Aug 22, 2013 at 21:34 JayJay 19.8k11 gold badges54 silver badges72 bronze badges 3
  • 6 Have you examined the property descriptors for the properties in the error object? – Ray Toal Commented Aug 22, 2013 at 21:37
  • 5 The question for me was 'why', and I found the answer was at the bottom of the question. There's nothing wrong with posting an answer for your own question, and you'll probably get more cred that way. :-) – Michael Scheper Commented Dec 1, 2014 at 5:04
  • 1 The serialize-error package handles this for you: npmjs.com/package/serialize-error – tim-phillips Commented Aug 24, 2020 at 20:06
Add a comment  | 

15 Answers 15

Reset to default 550
JSON.stringify(err, Object.getOwnPropertyNames(err))

seems to work

[from a comment by /u/ub3rgeek on /r/javascript] and felixfbecker's comment below

Also see the answer by "Sanghyun Lee" for an explanation why this is required.

As no one is talking about the why part, I'm gonna answer it.

Why this JSON.stringify returns an empty object?

> JSON.stringify(error);
'{}'

Answer

From the document of JSON.stringify(),

For all the other Object instances (including Map, Set, WeakMap and WeakSet), only their enumerable properties will be serialized.

and Error object doesn't have any enumerable properties, that's why it prints an empty object.

Background on enumerable properties

In Javascript, an object can have two types of properties:

  • enumerable properties
  • non-enumerable properties

The exact distinction is a bit tricky, but basically:

  • "normal" properties, such as the ones you create by assignment ( myobj= {}; myobj.prop1 = 4711;), are enumerable,
  • "internal" properties, such as the length property of an array, are non-enumerable

In particular, an Error has only non-enumerable properties.

For details, see for example Enumerability and ownership of properties on MDN.

You can define a Error.prototype.toJSON to retrieve a plain Object representing the Error:

if (!('toJSON' in Error.prototype))
Object.defineProperty(Error.prototype, 'toJSON', {
    value: function () {
        var alt = {};

        Object.getOwnPropertyNames(this).forEach(function (key) {
            alt[key] = this[key];
        }, this);

        return alt;
    },
    configurable: true,
    writable: true
});
var error = new Error('testing');
error.detail = 'foo bar';

console.log(JSON.stringify(error));
// {"message":"testing","detail":"foo bar"}

Using Object.defineProperty() adds toJSON without it being an enumerable property itself.


Regarding modifying Error.prototype, while toJSON() may not be defined for Errors specifically, the method is still standardized for objects in general (ref: step 3). So, the risk of collisions or conflicts is minimal.

Though, to still avoid it completely, JSON.stringify()'s replacer parameter can be used instead:

function replaceErrors(key, value) {
    if (value instanceof Error) {
        var error = {};

        Object.getOwnPropertyNames(value).forEach(function (propName) {
            error[propName] = value[propName];
        });

        return error;
    }

    return value;
}

var error = new Error('testing');
error.detail = 'foo bar';

console.log(JSON.stringify(error, replaceErrors));

There is a great Node.js package for that: serialize-error.

npm install serialize-error

It handles well even nested Error objects.

import {serializeError} from 'serialize-error';

const stringifiedError = serializeError(error);

Docs: https://www.npmjs.com/package/serialize-error

Modifying Jonathan's great answer to avoid monkey patching:

var stringifyError = function(err, filter, space) {
  var plainObject = {};
  Object.getOwnPropertyNames(err).forEach(function(key) {
    plainObject[key] = err[key];
  });
  return JSON.stringify(plainObject, filter, space);
};

var error = new Error('testing');
error.detail = 'foo bar';

console.log(stringifyError(error, null, '\t'));

We needed to serialise an arbitrary object hierarchy, where the root or any of the nested properties in the hierarchy could be instances of Error.

Our solution was to use the replacer param of JSON.stringify(), e.g.:

function jsonFriendlyErrorReplacer(key, value) {
  if (value instanceof Error) {
    return {
      // Pull all enumerable properties, supporting properties on custom Errors
      ...value,
      // Explicitly pull Error's non-enumerable properties
      name: value.name,
      message: value.message,
      stack: value.stack,
    }
  }

  return value
}

let obj = {
    error: new Error('nested error message')
}

console.log('Result WITHOUT custom replacer:', JSON.stringify(obj))
console.log('Result WITH custom replacer:', JSON.stringify(obj, jsonFriendlyErrorReplacer))

I was working on a JSON format for log appenders and ended up here trying to solve a similar problem. After a while, I realized I could just make Node do the work:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}

If using nodejs there is better reliable way by using native nodejs inspect. As well you can specify to print objects to unlimited depth.

Typescript example:

import { inspect }  from "util";

const myObject = new Error("This is error");
console.log(JSON.stringify(myObject)); // Will print {}
console.log(myObject); // Will print full error object
console.log(inspect(myObject, {depth: null})); // Same output as console.log plus it works as well for objects with many nested properties.

Link to documentation, link to example usage.

And as well discussed in the topic How can I get the full object in Node.js's console.log(), rather than '[Object]'? here in stack overflow.

You can also just redefine those non-enumerable properties to be enumerable.

Object.defineProperty(Error.prototype, 'message', {
    configurable: true,
    enumerable: true
});

and maybe stack property too.

String constructor should be able to stringify error

try { 
  throw new Error("MY ERROR MSG")
} catch (e) {
  String(e) // returns 'Error: MY ERROR MSG'
}

None of the answers above seemed to properly serialize properties which are on the prototype of Error (because getOwnPropertyNames() does not include inherited properties). I was also not able to redefine the properties like one of the answers suggested.

This is the solution I came up with - it uses lodash but you could replace lodash with generic versions of those functions.

 function recursivePropertyFinder(obj){
    if( obj === Object.prototype){
        return {};
    }else{
        return _.reduce(Object.getOwnPropertyNames(obj), 
            function copy(result, value, key) {
                if( !_.isFunction(obj[value])){
                    if( _.isObject(obj[value])){
                        result[value] = recursivePropertyFinder(obj[value]);
                    }else{
                        result[value] = obj[value];
                    }
                }
                return result;
            }, recursivePropertyFinder(Object.getPrototypeOf(obj)));
    }
}


Error.prototype.toJSON = function(){
    return recursivePropertyFinder(this);
}

Here's the test I did in Chrome:

var myError = Error('hello');
myError.causedBy = Error('error2');
myError.causedBy.causedBy = Error('error3');
myError.causedBy.causedBy.displayed = true;
JSON.stringify(myError);

{"name":"Error","message":"hello","stack":"Error: hello\n    at <anonymous>:66:15","causedBy":{"name":"Error","message":"error2","stack":"Error: error2\n    at <anonymous>:67:20","causedBy":{"name":"Error","message":"error3","stack":"Error: error3\n    at <anonymous>:68:29","displayed":true}}}  

Just convert to a regular object

// example error
let err = new Error('I errored')

// one liner converting Error into regular object that can be stringified
err = Object.getOwnPropertyNames(err).reduce((acc, key) => { acc[key] = err[key]; return acc; }, {})

If you want to send this object from child process, worker or though the network there's no need to stringify. It will be automatically stringified and parsed like any other normal object

I've extended this answer: Is it not possible to stringify an Error using JSON.stringify?

serializeError.ts

export function serializeError(err: unknown) {
    return JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err)))
}

And I can use it like this:

import { serializeError } from '../helpers/serializeError'; // Change to your path

try {
    const res = await create(data);
    return { status: 201 };
} catch (err) {
    return { status: 400, error: serializeError(err) };
}

Usually I declare once:

  const cloneError = (err) => {
    return err ? { name: err.name, message: err.message, stack: err.stack, cause: err.cause } : {};
  };

Then everywhere I can use it, for instance:

...logger.log('An error occurred:', cloneError(err));

You can solve this with a one-liner( errStringified ) in plain javascript:

var error = new Error('simple error message');
var errStringified = (err => JSON.stringify(Object.getOwnPropertyNames(Object.getPrototypeOf(err)).reduce(function(accumulator, currentValue) { return accumulator[currentValue] = err[currentValue], accumulator}, {})))(error);
console.log(errStringified);

It works with DOMExceptions as well.

本文标签: javascriptIs it not possible to stringify an Error using JSONstringifyStack Overflow