Não é possível restringir um erro usando JSON.stringify?

330

Reproduzindo o problema

Estou com um problema ao tentar passar mensagens de erro usando soquetes da web. Posso replicar o problema que estou enfrentando JSON.stringifypara atender a um público mais amplo:

// 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);
    '{}'

O problema é que eu acabo com um objeto vazio.

O que eu tentei

Navegadores

Tentei sair do node.js e executá-lo em vários navegadores. A versão 28 do Chrome me dá o mesmo resultado e, curiosamente, o Firefox pelo menos tenta, mas deixa de fora a mensagem:

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

Função substituidora

Então olhei para o erro.protótipo . Isso mostra que o protótipo contém métodos como toString e toSource . Sabendo que as funções não podem ser restringidas, incluí uma função substituta ao chamar JSON.stringify para remover todas as funções, mas depois percebi que ela também tinha um comportamento estranho:

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

Parece não fazer um loop sobre o objeto como faria normalmente e, portanto, não posso verificar se a chave é uma função e ignorá-la.

A questão

Existe alguma maneira de restringir mensagens de erro nativas com JSON.stringify? Caso contrário, por que esse comportamento ocorre?

Métodos para contornar isso

  • Atenha-se a mensagens de erro simples baseadas em string ou crie objetos de erro pessoais e não confie no objeto Error nativo.
  • Propriedades de extração: JSON.stringify({ message: error.message, stack: error.stack })

Atualizações

@Ray Toal Sugerido em um comentário que dê uma olhada nos descritores de propriedades . Agora está claro por que não funciona:

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

Resultado:

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.

A resposta aceita fornece uma solução alternativa para esse problema.

JayQuerie.com
fonte
3
Você examinou os descritores de propriedades para as propriedades no objeto de erro?
quer
3
A pergunta para mim era 'por que', e achei que a resposta estava no final da pergunta. Não há nada errado em postar uma resposta para sua própria pergunta, e você provavelmente obterá mais crédito dessa maneira. :-)
Michael Scheper

Respostas:

178

Você pode definir a Error.prototype.toJSONpara recuperar uma planície Objectrepresentando 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"}

O uso de Object.defineProperty()adiciona toJSONsem que seja uma enumerablepropriedade em si.


Em relação à modificação Error.prototype, embora toJSON()não possa ser definido Errorespecificamente para s, o método ainda é padronizado para objetos em geral (ref: etapa 3). Portanto, o risco de colisões ou conflitos é mínimo.

Porém, para evitá-lo completamente, JSON.stringify()o replacerparâmetro pode ser usado:

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));
Jonathan Lonowski
fonte
3
Se você usar em .getOwnPropertyNames()vez de .keys(), obterá propriedades não enumeráveis ​​sem precisar defini-las manualmente.
8
É melhor não adicionar ao Error.prototype, pois isso pode causar problemas quando, em uma versão futura do JavaScrip, o Error.prototype realmente tiver uma função toJSON.
Jos de Jong
3
Cuidado! Esta quebra de soluções de tratamento de erro no driver de nó mongodb nativa: jira.mongodb.org/browse/NODE-554
Sebastian Nowak
5
Caso alguém preste atenção nos erros do vinculador e nos conflitos de nomenclatura: se estiver usando a opção substituto, escolha um nome de parâmetro diferente para keyin function replaceErrors(key, value)para evitar conflitos de nomenclatura .forEach(function (key) { .. }); o replaceErrors keyparâmetro não é utilizado nesta resposta.
404 Não encontrado
2
A sombra keydeste exemplo, embora permitida, é potencialmente confusa, pois deixa dúvidas se o autor pretendia se referir à variável externa ou não. propNameseria uma escolha mais expressiva para o loop interno. (BTW, acho que @ 404NotFound significava "linter" (ferramenta de análise estática) e não "linker" ). De qualquer forma, o uso de uma replacerfunção personalizada é uma excelente solução para isso, pois resolve o problema em um local apropriado e não altera os recursos nativos. / comportamento global.
iX3 20/09/19
261
JSON.stringify(err, Object.getOwnPropertyNames(err))

parece funcionar

[ de um comentário de / u / ub3rgeek em / r / javascript ] e o comentário de felixfbecker abaixo

atraso
fonte
57
Pentear as respostas,JSON.stringify(err, Object.getOwnPropertyNames(err))
felixfbecker
5
Isso funciona bem para um objeto de erro ExpressJS nativo, mas não funciona com um erro do Mongoose. Erros de mangusto aninharam objetos para ValidationErrortipos. Isso não especificará o errorsobjeto aninhado em um objeto de erro do Mongoose do tipo ValidationError.
steampowered
4
essa deve ser a resposta, porque é a maneira mais simples de fazer isso.
Huan
7
@felixfbecker Isso procura apenas nomes de propriedades em um nível . Se você tiver var spam = { a: 1, b: { b: 2, b2: 3} };e executar Object.getOwnPropertyNames(spam), ficará ["a", "b"]enganoso aqui, porque o bobjeto possui o seu b. Você receberia os dois em sua ligação com string, mas perderiaspam.b.b2 . Isso é ruim.
ruffin
1
@ Ruffin isso é verdade, mas pode até ser desejável. Acho que o que o OP queria era apenas ter certeza messagee stackestar incluído no JSON.
Felixfbecker
74

Como ninguém está falando sobre a parte do porquê , eu vou responder.

Por que isso JSON.stringifyretorna um objeto vazio?

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

Responda

No documento JSON.stringify () ,

Para todas as outras instâncias de objeto (incluindo Map, Set, WeakMap e WeakSet), apenas suas propriedades enumeráveis ​​serão serializadas.

e o Errorobjeto não tem suas propriedades enumeráveis, é por isso que imprime um objeto vazio.

Sanghyun Lee
fonte
4
Estranho, ninguém se incomodou. Enquanto correção funciona Presumo :)
Ilya Chernomordik
1
A primeira parte desta resposta não está correta. Existe uma maneira de usar JSON.stringifyusando seu replacerparâmetro.
Todd Chaffee
1
@ToddChaffee esse é um bom ponto. Eu consertei minha resposta. Por favor, verifique e sinta-se livre para melhorá-lo. Obrigado.
Sanghyun Lee
52

Modificando a ótima resposta de Jonathan para evitar o patch de macacos:

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'));
Bryan Larsen
fonte
3
Primeira vez que eu ouvi monkey patching:)
Chris Príncipe
2
@ChrisPrince Mas não será a última vez, especialmente em JavaScript! Aqui está a Wikipedia sobre Monkey Patching , apenas para informações futuras. (Em resposta de Jonathan , como Chris entende, você está adicionando uma nova função, toJSON, diretamente ao Errorprotótipo 's , que muitas vezes não é uma ótima idéia. Talvez alguém já tem, que este cheques, mas então você não sabe o que que outra versão faz ou se alguém inesperadamente recebe seu, ou assume protótipo de erro tem propriedades específicas, as coisas poderiam Bork)..
ruffin
isso é legal, mas omite a pilha do erro (que é mostrada no console). não tenho certeza dos detalhes, se isso é relacionado ao Vue ou o quê, só queria mencionar isso.
22419 philip4
23

Há um grande pacote Node.js para que: serialize-error.

Ele lida com objetos de erro bem aninhados, o que eu realmente precisava de muito no meu projeto.

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

Lukasz Czerwinski
fonte
Não, mas pode ser transpilado para isso. Veja este comentário .
iX3 20/09/19
Esta é a resposta correta. Serializar erros não é um problema trivial, e o autor da biblioteca (um excelente desenvolvedor com muitos pacotes altamente populares) se esforçou bastante para lidar com casos extremos, como pode ser visto no README: "Propriedades personalizadas são preservadas. Não enumeráveis as propriedades são mantidas não enumeráveis ​​(nome, mensagem, pilha). As propriedades enumeráveis ​​são mantidas enumeráveis ​​(todas as propriedades além das não enumeráveis). As referências circulares são manipuladas. "
Dan Dascalescu
9

Você também pode redefinir as propriedades não enumeráveis ​​para serem enumeráveis.

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

e talvez stackpropriedade também.

cheolgook
fonte
9
Não altere objetos que você não possui, pois isso pode quebrar outras partes do aplicativo e boa sorte para descobrir o porquê.
21719 fregante
7

Precisávamos serializar uma hierarquia arbitrária de objetos, onde a raiz ou qualquer uma das propriedades aninhadas na hierarquia poderia ser instâncias de Error.

Nossa solução foi usar os replacerparâmetros de JSON.stringify(), por exemplo:

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

Joel Malone
fonte
5

Nenhuma das respostas acima parecia serializar corretamente as propriedades que estão no protótipo de Erro (porque getOwnPropertyNames()não inclui propriedades herdadas). Também não consegui redefinir as propriedades como uma das respostas sugeridas.

Esta é a solução que eu encontrei - ele usa o lodash, mas você pode substituí-lo por versões genéricas dessas funções.

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

Aqui está o teste que fiz no 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}}}  
Elliott Palermo
fonte
2

Eu estava trabalhando em um formato JSON para anexadores de log e acabei aqui tentando resolver um problema semelhante. Depois de um tempo, percebi que podia fazer o Node fazer o trabalho:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}
Jason
fonte
1
Deveria ser instanceofe não instanceOf.
lakshman.pasala 22/04