N'est-il pas possible de stringifier une erreur en utilisant JSON.stringify?

331

Reproduire le problème

Je rencontre un problème lorsque j'essaie de transmettre des messages d'erreur concernant l'utilisation des sockets Web. Je peux reproduire le problème auquel je fais face JSON.stringifypour répondre à un public plus large:

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

Le problème est que je me retrouve avec un objet vide.

Ce que j'ai essayé

Navigateurs

J'ai d'abord essayé de quitter node.js et de l'exécuter dans divers navigateurs. La version 28 de Chrome me donne le même résultat et, chose intéressante, Firefox fait au moins une tentative mais laisse de côté le message:

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

Fonction de remplacement

J'ai ensuite regardé le prototype Error.prototype . Il montre que le prototype contient des méthodes telles que toString et toSource . Sachant que les fonctions ne peuvent pas être stringifiées, j'ai inclus une fonction de remplacement lors de l'appel de JSON.stringify pour supprimer toutes les fonctions, mais ensuite réalisé qu'elle avait également un comportement étrange:

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

Il ne semble pas boucler sur l'objet comme il le ferait normalement, et donc je ne peux pas vérifier si la clé est une fonction et l'ignorer.

La question

Existe-t-il un moyen de filtrer les messages d'erreur natifs avec JSON.stringify ? Sinon, pourquoi ce problème se produit-il?

Méthodes pour contourner ce problème

  • Restez avec des messages d'erreur simples basés sur des chaînes ou créez des objets d'erreur personnels et ne vous fiez pas à l'objet d'erreur natif.
  • Propriétés de traction: JSON.stringify({ message: error.message, stack: error.stack })

Mises à jour

@Ray Toal Suggéré dans un commentaire que je regarde les descripteurs de propriété . Il est maintenant clair pourquoi cela ne fonctionne pas:

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

Production:

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.

La réponse acceptée fournit une solution de contournement à ce problème.

JayQuerie.com
la source
3
Avez-vous examiné les descripteurs de propriété pour les propriétés de l'objet d'erreur?
Ray Toal
3
La question pour moi était «pourquoi», et j'ai trouvé que la réponse était au bas de la question. Il n'y a rien de mal à publier une réponse à votre propre question, et vous obtiendrez probablement plus de crédit de cette façon. :-)
Michael Scheper

Réponses:

179

Vous pouvez définir un Error.prototype.toJSONpour récupérer une plaine Objectreprésentant 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"}

Utiliser Object.defineProperty()ajoute toJSONsans que ce soit une enumerablepropriété elle-même.


En ce qui concerne la modification Error.prototype, même si elle toJSON()ne peut pas être définie Errorspécifiquement pour s, la méthode est toujours normalisée pour les objets en général (réf: étape 3). Ainsi, le risque de collisions ou de conflits est minime.

Cependant, pour l'éviter complètement, JSON.stringify()le replacerparamètre de peut être utilisé à la place:

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
la source
3
Si vous utilisez .getOwnPropertyNames()au lieu de .keys(), vous obtiendrez les propriétés non énumérables sans avoir à les définir manuellement.
8
Mieux vaut ne pas ajouter au fichier Error.prototype, peut donner des problèmes lorsque dans une future version de JavaScrip le fichier Error.prototype a réellement une fonction toJSON.
Jos de Jong
3
Prudent! Cette solution rompt la gestion des erreurs dans le pilote mongodb du
Sebastian Nowak
5
Si quelqu'un prête attention à ses erreurs de l'éditeur de liens et à ses conflits de dénomination: si vous utilisez l'option de remplacement, vous devez choisir un nom de paramètre différent pour keyin function replaceErrors(key, value)afin d'éviter tout conflit de dénomination avec .forEach(function (key) { .. }); le replaceErrors keyparamètre n'est pas utilisé dans cette réponse.
404 Introuvable
2
L'observation de keydans cet exemple, bien qu'autorisée, est potentiellement déroutante car elle laisse planer le doute quant à savoir si l'auteur avait l'intention de se référer à la variable externe ou non. propNameserait un choix plus expressif pour la boucle intérieure. (BTW, je pense que @ 404NotFound signifiait « linter » (outil d'analyse statique) pas « linker » ) Dans tous les cas, en utilisant une coutume replacerfonction est une excellente solution pour cela car il résout le problème en un seul, le lieu approprié et ne modifie pas natif / comportement global.
iX3
262
JSON.stringify(err, Object.getOwnPropertyNames(err))

semble fonctionner

[ extrait d'un commentaire de / u / ub3rgeek sur / r / javascript ] et du commentaire de felixfbecker ci-dessous

retard de réflexion
la source
57
Peigner les réponses,JSON.stringify(err, Object.getOwnPropertyNames(err))
felixfbecker
5
Cela fonctionne bien pour un objet d'erreur ExpressJS natif, mais cela ne fonctionnera pas avec une erreur Mongoose. Les erreurs de mangouste ont des objets imbriqués pour les ValidationErrortypes. Cela ne stringifiera pas l' errorsobjet imbriqué dans un objet d'erreur Mongoose de type ValidationError.
Steam Powered
4
cela devrait être la réponse, car c'est le moyen le plus simple de le faire.
Huan
7
@felixfbecker Qui ne recherche que les noms de propriété d' un niveau . Si vous avez var spam = { a: 1, b: { b: 2, b2: 3} };et courez Object.getOwnPropertyNames(spam), vous serez ["a", "b"]trompé ici, car l' bobjet a le sien b. Vous obtiendrez les deux dans votre appel stringify, mais vous manqueriezspam.b.b2 . C'est mauvais.
ruffin
1
@ruffin c'est vrai, mais cela pourrait même être souhaitable. Je pense que ce que voulait OP était juste de s'assurer messageet stacksont inclus dans le JSON.
felixfbecker
74

Comme personne ne parle de la partie pourquoi , je vais y répondre.

Pourquoi cela JSON.stringifyrenvoie un objet vide?

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

Répondre

Du document de JSON.stringify () ,

Pour toutes les autres instances d'objet (y compris Map, Set, WeakMap et WeakSet), seules leurs propriétés énumérables seront sérialisées.

et l' Errorobjet n'a pas ses propriétés énumérables, c'est pourquoi il imprime un objet vide.

Sanghyun Lee
la source
4
Étrange, personne n'a même dérangé. Tant que le correctif fonctionne, je suppose :)
Ilya Chernomordik
1
La première partie de cette réponse n'est pas correcte. Il existe un moyen d'utiliser JSON.stringifyson replacerparamètre.
Todd Chaffee
1
@ToddChaffee c'est un bon point. J'ai corrigé ma réponse. Veuillez le vérifier et n'hésitez pas à l'améliorer. Merci.
Sanghyun Lee
52

Modification de la bonne réponse de Jonathan pour éviter les correctifs de singe:

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
la source
3
Première fois que j'entends monkey patching:)
Chris Prince
2
@ChrisPrince Mais ce ne sera pas la dernière fois, surtout en JavaScript! Voici Wikipedia sur Monkey Patching , juste pour les informations des futurs. (Dans la réponse de Jonathan , comme Chris comprend, vous ajoutez une nouvelle fonction, toJSON, directement au Errorprototype de » , qui est souvent pas une bonne idée. Peut - être que quelqu'un d' autre a déjà, qui ce contrôle, mais vous ne savez pas ce que cette autre version le fait. Ou si quelqu'un obtient le vôtre de façon inattendue, ou suppose que le prototype d'Erreur a des propriétés spécifiques, les choses pourraient s'arrêter.)
ruffin
c'est bien, mais omet la pile de l'erreur (qui est affichée dans la console). pas sûr des détails, si c'est lié à Vue ou quoi, je voulais juste le mentionner.
phil294
23

Il existe un excellent package Node.js pour cela: serialize-error .

Il gère bien même les objets Error imbriqués, ce dont j'avais réellement besoin dans mon projet.

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

Lukasz Czerwinski
la source
Non, mais il peut être transcrit pour le faire. Voir ce commentaire .
iX3
Ceci est la bonne réponse. La sérialisation des erreurs n'est pas un problème trivial, et l'auteur de la bibliothèque (un excellent développeur avec de nombreux packages très populaires) s'est donné beaucoup de mal pour gérer les cas marginaux, comme on peut le voir dans le README: "Les propriétés personnalisées sont préservées. Non énumérable les propriétés sont conservées non énumérables (nom, message, pile). Les propriétés énumérables sont conservées énumérables (toutes les propriétés autres que celles non énumérables). Les références circulaires sont gérées. "
Dan Dascalescu
9

Vous pouvez également simplement redéfinir ces propriétés non énumérables pour qu'elles soient énumérables.

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

et peut-être stackaussi la propriété.

cheolgook
la source
9
Ne changez pas les objets que vous ne possédez pas, cela peut casser d'autres parties de votre application et bonne chance pour trouver pourquoi.
fregante
7

Nous avions besoin de sérialiser une hiérarchie d'objets arbitraires, où la racine ou l'une des propriétés imbriquées dans la hiérarchie pourrait être des instances d'Erreur.

Notre solution a été d'utiliser les replacerparamètres de JSON.stringify(), par exemple:

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
la source
5

Aucune des réponses ci-dessus ne semble sérialiser correctement les propriétés qui se trouvent sur le prototype de l'erreur (car getOwnPropertyNames() n'inclut pas les propriétés héritées). Je n'ai pas non plus été en mesure de redéfinir les propriétés comme l'une des réponses suggérées.

C'est la solution que j'ai trouvée - elle utilise lodash mais vous pouvez remplacer lodash par des versions génériques de ces fonctions.

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

Voici le test que j'ai fait dans 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
la source
2

Je travaillais sur un format JSON pour les ajouts de journaux et j'ai fini par essayer de résoudre un problème similaire. Après un certain temps, j'ai réalisé que je pouvais simplement faire faire le travail à Node:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}
Jason
la source
1
Cela devrait être instanceofet noninstanceOf .
lakshman.pasala