关于javascript:是不是可以使用JSON.stringify字符串化错误?

Is it not possible to stringify an Error using JSON.stringify?

再现问题

我在尝试使用Web套接字传递错误消息时遇到了一个问题。我可以用JSON.stringify复制我面临的问题,以迎合更广泛的受众:

1
2
3
4
5
6
7
8
9
10
11
12
// 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);
    '{}'

问题是我最终得到了一个空对象。

我试过什么

浏览器

我首先尝试离开node.js并在各种浏览器中运行它。Chrome28版给了我同样的结果,有趣的是,火狐至少尝试了一下,但忽略了以下信息:

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

替换程序功能

然后我查看了错误。原型。结果表明,原型包含ToString和ToSource等方法。我知道函数不能串化,所以在调用json.stringify删除所有函数时,我包含了一个replacer函数,但后来意识到它也有一些奇怪的行为:

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

它看起来不像通常那样循环对象,因此我无法检查键是否是函数并忽略它。

问题

有没有办法用JSON.stringify来串接本机错误消息?如果没有,为什么会发生这种行为?

解决这个问题的方法

  • 坚持使用简单的基于字符串的错误消息,或者创建个人错误对象,而不依赖于本机错误对象。
  • 拉伸性能:JSON.stringify({ message: error.message, stack: error.stack })

更新

@RayToal在评论中建议我看一下属性描述符。现在很清楚为什么它不起作用:

1
2
3
4
5
6
7
8
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);
}

输出:

4

密钥:enumerable: false

接受的答案为这个问题提供了解决方法。


1
JSON.stringify(err, Object.getOwnPropertyNames(err))

似乎工作

[来自/u/ub3rgeek on/r/javascript的评论]和下面的felixfbecker的评论


可以定义Error.prototype.toJSON来检索表示Error的普通Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
});
1
2
3
4
5
var error = new Error('testing');
error.detail = 'foo bar';

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

使用Object.defineProperty()添加toJSON,而不是enumerable属性本身。

关于修改Error.prototype,虽然toJSON()可能没有为Error定义,但该方法仍然是一般对象的标准化方法(参考:步骤3)。因此,碰撞或冲突的风险是最小的。

不过,为了完全避免这种情况,可以使用JSON.stringify()replacer参数来代替:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function replaceErrors(key, value) {
    if (value instanceof Error) {
        var error = {};

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

        return error;
    }

    return value;
}

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

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


修改乔纳森的伟大答案以避免猴子修补:

1
2
3
4
5
6
7
8
9
10
11
12
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'));


因为没人在谈论为什么,我要回答这些问题

问:有没有办法用json.stringify将本机错误消息串接起来?

不。

问:如果没有,为什么会发生这种行为?

从json.stringify()的文档中,

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

Error对象没有其可枚举的属性,这就是它打印空对象的原因。


有一个很棒的node.js包:serialize-error

它甚至可以很好地处理嵌套的错误对象,这是我在项目中实际需要的。

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


您也可以将那些不可枚举的属性重新定义为可枚举的。

4

也可能是stack的财产。


上面的回答似乎都没有正确地序列化错误原型上的属性(因为getOwnPropertyNames()不包括继承的属性)。我也无法像其中一个答案所建议的那样重新定义属性。

这是我提出的解决方案——它使用了lodash,但您可以用这些函数的通用版本替换lodash。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 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);
}

这是我在Chrome上做的测试:

1
2
3
4
5
6
7
8
9
10
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
    at :66:15"
,"causedBy":{"name":"Error","message":"error2","stack":"Error: error2
    at :67:20"
,"causedBy":{"name":"Error","message":"error3","stack":"Error: error3
    at :68:29"
,"displayed":true}}}

我们需要序列化一个任意的对象层次结构,其中根或层次结构中的任何嵌套属性都可能是错误的实例。

我们的解决方案是使用JSON.stringify()replacer参数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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))