Разве невозможно зашифровать ошибку с помощью JSON.stringify?

331

Воспроизведение проблемы

Я столкнулся с проблемой при попытке передать сообщения об ошибках с помощью веб-сокетов. Я могу повторить проблему, с которой сталкиваюсь, JSON.stringifyчтобы удовлетворить более широкую аудиторию:

// 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 и запустить его в разных браузерах. Chrome версии 28 дает мне тот же результат, и, что интересно, Firefox по крайней мере делает попытку, но пропускает сообщение:

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

Функция заменителя

Затем я посмотрел на Error.prototype . Это показывает, что прототип содержит такие методы, как toString и toSource . Зная, что функции не могут быть строковыми, я включил функцию- заменитель при вызове JSON.stringify, чтобы удалить все функции, но затем понял, что у него тоже странное поведение:

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

Кажется, он не зацикливается на объекте, как обычно, и поэтому я не могу проверить, является ли ключ функцией, и игнорировать его.

Вопрос

Есть ли способ привести в порядок собственные сообщения об ошибках JSON.stringify? Если нет, то почему это происходит?

Методы обойти это

  • Придерживайтесь простых сообщений об ошибках на основе строк или создавайте личные объекты ошибок и не полагайтесь на собственный объект Error.
  • Тянуть свойства: JSON.stringify({ message: error.message, stack: error.stack })

Обновления

@Ray Toal Предложено в комментарии, что я смотрю на дескрипторы свойств . Теперь понятно, почему это не работает:

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);
}

Вывод:

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 }

Ключ: enumerable: false.

Принятый ответ обеспечивает обходной путь для этой проблемы.

JayQuerie.com
источник
3
Вы изучили дескрипторы свойств для свойств объекта ошибки?
Рэй Тоал
3
Вопрос для меня был «почему», и я обнаружил, что ответ был в нижней части вопроса. Нет ничего плохого в том, чтобы опубликовать ответ на свой вопрос, и вы, вероятно, получите больше доверия таким образом. :-)
Майкл Шепер

Ответы:

180

Вы можете определить Error.prototype.toJSONдля получения простой, Objectпредставляющей 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"}

Использование Object.defineProperty()добавляет, toJSONне будучи enumerableсамим свойством.


Что касается модификации Error.prototype, хотя она toJSON()не может быть определена специально для Errors, метод все еще стандартизирован для объектов в целом (см. Шаг 3). Таким образом, риск столкновений или конфликтов минимален.

Хотя, по - прежнему избегать его полностью, JSON.stringify()«s replacerпараметр может быть использован вместо:

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));
Джонатан Лоновски
источник
3
Если вы используете .getOwnPropertyNames()вместо .keys(), вы получите неперечислимые свойства без необходимости определять их вручную.
8
Лучше не добавлять в прототип Error.prototype, это может вызвать проблемы, когда в будущей версии JavaScrip этот Error.prototype действительно имеет функцию toJSON.
Йос де Йонг,
3
Осторожный! Это решение нарушает обработку ошибок в драйвере нодного
Себастьян Новак
5
В случае, если кто-то обращает внимание на ошибки компоновщика и конфликты имен: при использовании параметра replacer вы должны выбрать другое имя параметра для keyin, function replaceErrors(key, value)чтобы избежать конфликта имен с .forEach(function (key) { .. }); replaceErrors keyпараметр не используется в этом ответе.
404 Не найдено
2
Затенение keyв этом примере, хотя и разрешено, потенциально сбивает с толку, поскольку оставляет сомнение в том, намеревался ли автор ссылаться на внешнюю переменную или нет. propNameбудет более выразительным выбором для внутреннего цикла. (Кстати, я думаю, что @ 404NotFound означало «линтер» (инструмент статического анализа), а не «компоновщик» ). В любом случае, использование пользовательской replacerфункции является отличным решением для этой проблемы, поскольку она решает проблему в одном подходящем месте и не меняет нативную функцию. / глобальное поведение.
iX3
262
JSON.stringify(err, Object.getOwnPropertyNames(err))

похоже на работу

[ из комментария / u / ub3rgeek к / r / javascript ] и комментария felixfbecker ниже

laggingreflex
источник
57
Расчесывать ответы,JSON.stringify(err, Object.getOwnPropertyNames(err))
felixfbecker
5
Это прекрасно работает для нативного объекта Error ExpressJS, но не будет работать с ошибкой Mongoose. Ошибки Mongoose имеют вложенные объекты для ValidationErrorтипов. Это не приведёт в порядок вложенный errorsобъект в объект ошибки типа Mongoose ValidationError.
steampowered
4
это должен быть ответ, потому что это самый простой способ сделать это.
Хуан,
7
@felixfbecker Это ищет имена свойств только на один уровень . Если у вас есть var spam = { a: 1, b: { b: 2, b2: 3} };и работает Object.getOwnPropertyNames(spam), вы получите ["a", "b"]- обманчиво здесь, потому что bобъект имеет свой собственный b. Вы получите оба в своем звонке, но вы пропуститеspam.b.b2 . Это плохо.
ruffin
1
@ ruffin это правда, но это может быть даже желательно. Я думаю, что OP хотел, чтобы убедиться, messageи stackвключены в JSON.
felixfbecker,
74

Поскольку никто не говорит о части « почему» , я отвечу на нее.

Почему это JSON.stringifyвозвращает пустой объект?

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

Ответ

Из документа JSON.stringify () ,

Для всех других экземпляров Object (включая Map, Set, WeakMap и WeakSet) только их перечисляемые свойства будут сериализованы.

и Errorобъект не имеет своих перечисляемых свойств, поэтому он печатает пустой объект.

Санхьюн Ли
источник
4
Странно, никто даже не беспокоился. Пока исправление работает, я предполагаю :)
Илья Черномордик
1
Первая часть этого ответа не верна. Есть способ использовать JSON.stringifyего replacerпараметр.
Тодд Чаффи
1
@ToddChaffee это хорошая мысль. Я исправил свой ответ. Пожалуйста, проверьте его и не стесняйтесь, чтобы улучшить его. Спасибо.
Санхьюн Ли
52

Изменение отличного ответа Джонатана, чтобы избежать исправлений обезьяны:

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'));
Брайан Ларсен
источник
3
Впервые слышу monkey patching:)
Крис Принс
2
@ChrisPrince Но это будет не последний раз, особенно в JavaScript! Вот Википедия по Patching Monkey , только для информации будущих людей. (В ответ Джонатана , так как понимает , Крис, вы добавляете новую функцию, toJSON, непосредственно Errorпрототип «S , который часто не хорошая идея. Может быть , кто - то уже есть, которых это проверяет, но вы не знаете , что что делает другая версия. Или, если кто-то неожиданно получит вашу или предположит, что прототип Error обладает особыми свойствами, все может испортиться.)
ruffin
это хорошо, но пропускает стек ошибки (который показан в консоли). не уверен в деталях, если это связано с Vue или что, просто хотел бы упомянуть об этом.
phil294
23

Существует большой пакет Node.js для этого: serialize-error.

Он хорошо обрабатывает даже вложенные объекты Error, что мне действительно нужно в моем проекте.

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

Лукаш Червинский
источник
Нет, но это можно перенести для этого. Смотрите этот комментарий .
iX3
Это правильный ответ. Сериализация ошибок не является тривиальной проблемой, и автор библиотеки (превосходный разработчик со многими популярными пакетами) приложил значительные усилия для обработки крайних случаев, как можно увидеть в README: «Пользовательские свойства сохраняются. Не перечисляемый свойства остаются не перечисляемыми (имя, сообщение, стек). Перечисляемые свойства остаются перечисляемыми (все свойства, кроме не перечисляемых). Циркулярные ссылки обрабатываются. "
Дан Дакалеску
9

Вы также можете просто переопределить эти не перечисляемые свойства, чтобы они были перечисляемыми.

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

и, возможно, stackсобственность тоже.

cheolgook
источник
9
Не меняйте объекты, которые вам не принадлежат, это может сломать другие части вашего приложения и удачи в поиске причин
Фреган
7

Нам нужно было сериализовать произвольную иерархию объектов, где корень или любое из вложенных свойств в иерархии могут быть экземплярами Error.

Нашим решением было использовать replacerпараметр JSON.stringify(), например:

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))

Джоэл Мэлоун
источник
5

Похоже, что ни один из приведенных выше ответов не мог правильно сериализовать свойства, относящиеся к прототипу Error (поскольку getOwnPropertyNames()он не включает унаследованные свойства). Я также не смог переопределить свойства, как один из предложенных ответов.

Это решение, которое я придумал, - оно использует lodash, но вы можете заменить lodash на общие версии этих функций.

 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:

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}}}  
Эллиот Палермо
источник
2

Я работал над форматом JSON для добавителей журналов и в итоге попытался решить аналогичную проблему. Через некоторое время я понял, что могу просто заставить Node делать работу:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}
Джейсон
источник
1
Так должно быть instanceofи нет instanceOf.
lakshman.pasala