¿No es posible stringificar un error usando JSON.stringify?

331

Reproduciendo el problema

Me encuentro con un problema al intentar pasar mensajes de error usando los sockets web. Puedo replicar el problema que estoy usando JSON.stringifypara atender a un público más amplio:

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

El problema es que termino con un objeto vacío.

Lo que he intentado

Navegadores

Primero intenté dejar node.js y ejecutarlo en varios navegadores. La versión 28 de Chrome me da el mismo resultado y, curiosamente, Firefox por lo menos hace un intento pero dejó el mensaje:

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

Función de reemplazo

Luego miré el prototipo Error.prototype . Muestra que el prototipo contiene métodos como toString y toSource . Sabiendo que las funciones no pueden ser encadenadas, incluí una función de reemplazo al llamar a JSON.stringify para eliminar todas las funciones, pero luego me di cuenta de que también tenía un comportamiento extraño:

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

No parece recorrer el objeto como lo haría normalmente, y por lo tanto no puedo verificar si la tecla es una función e ignorarla.

La pregunta

¿Hay alguna forma de stringificar mensajes de error nativos con JSON.stringify? Si no, ¿por qué ocurre este comportamiento?

Métodos para evitar esto

  • Quédese con mensajes de error simples basados ​​en cadenas, o cree objetos de error personales y no confíe en el objeto de error nativo.
  • Propiedades de extracción: JSON.stringify({ message: error.message, stack: error.stack })

Actualizaciones

@ Ray Toal Sugerido en un comentario que eche un vistazo a los descriptores de propiedades . Ahora está claro por qué no 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);
}

Salida:

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 }

Clave: enumerable: false.

La respuesta aceptada proporciona una solución para este problema.

JayQuerie.com
fuente
3
¿Ha examinado los descriptores de propiedad de las propiedades en el objeto de error?
Ray Toal
3
La pregunta para mí era 'por qué', y encontré que la respuesta estaba al final de la pregunta. No hay nada de malo en publicar una respuesta para su propia pregunta, y probablemente obtendrá más crédito de esa manera. :-)
Michael Scheper

Respuestas:

178

Puede definir un Error.prototype.toJSONpara recuperar un plano que Objectrepresenta el 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"}

Usar Object.defineProperty()agrega toJSONsin que sea una enumerablepropiedad en sí misma.


Con respecto a la modificación Error.prototype, aunque toJSON()puede no estar definido Errorespecíficamente para s, el método todavía está estandarizado para los objetos en general (ref: paso 3). Por lo tanto, el riesgo de colisiones o conflictos es mínimo.

Sin embargo, todavía a evitarlo por completo, JSON.stringify()'s replacerparámetro se puede utilizar en su lugar:

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
fuente
3
Si usa en .getOwnPropertyNames()lugar de .keys(), obtendrá las propiedades no enumerables sin tener que definirlas manualmente.
8
Es mejor no agregarlo al Error.prototype, puede dar problemas cuando en una futura versión de JavaScrip el Error.prototype realmente tiene una función toJSON.
Jos de Jong
3
¡Cuidado! Esta solución interrumpe el manejo de errores en el controlador mongodb del nodo nativo: jira.mongodb.org/browse/NODE-554
Sebastian Nowak
55
En caso de que alguien presta atención a sus errores de enlace y los conflictos de nombres: si se utiliza la opción sustituto, usted debe elegir un nombre de parámetro diferente para keyde function replaceErrors(key, value)evitar conflicto de nombres con .forEach(function (key) { .. }); el replaceErrors keyparámetro no se usa en esta respuesta.
404 no encontrado el
2
El sombreado de keyen este ejemplo, aunque está permitido, es potencialmente confuso ya que deja dudas sobre si el autor pretendía referirse a la variable externa o no. propNamesería una opción más expresiva para el bucle interno. (Por cierto, creo que @ 404NotFound significa "linter" (herramienta de análisis estático) no "enlazador" ) En cualquier caso, el uso de una replacerfunción personalizada es una solución excelente para esto, ya que resuelve el problema en un lugar apropiado y no altera el nativo / comportamiento global.
iX3
261
JSON.stringify(err, Object.getOwnPropertyNames(err))

parece funcionar

[ de un comentario de / u / ub3rgeek en / r / javascript ] y el comentario de felixfbecker a continuación

laggingreflex
fuente
57
Peinando las respuestas,JSON.stringify(err, Object.getOwnPropertyNames(err))
felixfbecker
55
Esto funciona bien para un objeto de error ExpressJS nativo, pero no funcionará con un error Mongoose. Los errores de mangosta tienen objetos anidados para los ValidationErrortipos. Esto no stringificará el errorsobjeto anidado en un objeto de error Mongoose de tipo ValidationError.
impulsado por vapor el
44
Esta debería ser la respuesta, porque es la forma más sencilla de hacerlo.
Huan
77
@felixfbecker Eso solo busca nombres de propiedades de un nivel de profundidad . Si tienes var spam = { a: 1, b: { b: 2, b2: 3} };y corres Object.getOwnPropertyNames(spam), serás ["a", "b"]engañoso aquí, porque el bobjeto tiene lo suyo b. Obtendría ambos en su llamada de stringify, pero se perderíaspam.b.b2 . Eso es malo.
ruffin
1
@ruffin es cierto, pero incluso podría ser deseable. Creo que lo que OP quería era asegurarse messagey stackestán incluidos en el JSON.
felixfbecker
74

Como nadie está hablando de la parte del porqué , la responderé.

¿Por qué esto JSON.stringifydevuelve un objeto vacío?

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

Responder

Del documento de JSON.stringify () ,

Para todas las demás instancias de Object (incluidos Map, Set, WeakMap y WeakSet), solo se serializarán sus propiedades enumerables.

y el Errorobjeto no tiene sus propiedades enumerables, es por eso que imprime un objeto vacío.

Sanghyun Lee
fuente
44
Extraño, nadie se molestó. Siempre que la solución funcione, supongo :)
Ilya Chernomordik
1
La primera parte de esta respuesta no es correcta. Hay una manera de usar JSON.stringifyusando su replacerparámetro.
Todd Chaffee
1
@ToddChaffee es un buen punto. He arreglado mi respuesta. Por favor verifíquelo y no dude en mejorarlo. Gracias.
Sanghyun Lee
52

Modificando la gran respuesta de Jonathan para evitar parches de monos:

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
fuente
3
Primera vez que escucho monkey patching:)
Chris Prince
2
@ChrisPrince ¡Pero no será la última vez, especialmente en JavaScript! Aquí está Wikipedia en Monkey Patching , solo para información de futuras personas. (En la respuesta de Jonathan , como entiende Chris, va a añadir una nueva función, toJSON, directamente al Errorprototipo 's , que a menudo no es una gran idea. Tal vez alguien ya tiene, que este cheques, pero luego no saben qué esa otra versión sí. O si alguien inesperadamente consigue el tuyo, o asume que el prototipo de Error tiene propiedades específicas, las cosas podrían funcionar.)
ruffin
Esto es bueno, pero omite la pila del error (que se muestra en la consola). No estoy seguro de los detalles, si esto está relacionado con Vue o qué, solo quería mencionar esto.
phil294
23

Hay un gran paquete Node.js para que: serialize-error.

Maneja bien incluso los objetos de error anidados, lo que realmente necesitaba mucho en mi proyecto.

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

Lukasz Czerwinski
fuente
No, pero se puede transpirar para hacerlo. Ver este comentario .
iX3
Esta es la respuesta correcta. Serializar errores no es un problema trivial, y el autor de la biblioteca (un excelente desarrollador con muchos paquetes muy populares) hizo todo lo posible para manejar casos extremos, como se puede ver en el archivo README: "Las propiedades personalizadas se conservan. No enumerables las propiedades se mantienen no enumerables (nombre, mensaje, pila). Las propiedades enumerables se mantienen enumerables (todas las propiedades además de las no enumerables). Se manejan las referencias circulares ".
Dan Dascalescu
9

También puede redefinir esas propiedades no enumerables para que sean enumerables.

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

y quizás stackpropiedad también.

cheolgook
fuente
99
No cambie los objetos que no posee, puede romper otras partes de su aplicación y buena suerte al descubrir por qué.
Fregante
7

Necesitábamos serializar una jerarquía de objetos arbitraria, donde la raíz o cualquiera de las propiedades anidadas en la jerarquía podrían ser instancias de Error.

Nuestra solución fue usar el replacerparámetro de JSON.stringify(), por ejemplo:

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
fuente
5

Ninguna de las respuestas anteriores parece serializar correctamente las propiedades que están en el prototipo de Error (porque getOwnPropertyNames()no incluye las propiedades heredadas). Tampoco pude redefinir las propiedades como una de las respuestas sugeridas.

Esta es la solución que se me ocurrió: utiliza lodash, pero podría reemplazarlo con versiones genéricas de esas funciones.

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

Aquí está la prueba que hice en 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
fuente
2

Estaba trabajando en un formato JSON para agregadores de registro y terminé aquí tratando de resolver un problema similar. Después de un tiempo, me di cuenta de que podía hacer que Node hiciera el trabajo:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}
Jason
fuente
1
Debería ser instanceofy no instanceOf.
lakshman.pasala