Czy nie można ustalić błędu przy użyciu pliku JSON.stringify?

330

Odtwarzanie problemu

Występuje problem podczas próby przekazywania komunikatów o błędach przy użyciu gniazd sieciowych. Mogę powtórzyć problem, z którym się spotykam, JSON.stringifyaby dotrzeć do szerszego grona odbiorców:

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

Problem polega na tym, że kończę na pustym obiekcie.

Co próbowałem

Przeglądarki

Najpierw próbowałem opuścić node.js i uruchomić go w różnych przeglądarkach. Wersja Chrome 28 daje mi ten sam wynik i co ciekawe, Firefox przynajmniej próbuje, ale pomija komunikat:

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

Funkcja zamiennika

Następnie spojrzałem na prototyp Error.prototype . Pokazuje, że prototyp zawiera metody takie jak toString i toSource . Wiedząc, że funkcji nie można sprecyzować, do wywołania JSON.stringify zawarłem funkcję zamiennika, aby usunąć wszystkie funkcje, ale potem zdałem sobie sprawę, że również zachowuje się dziwnie:

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

Wydaje się, że nie zapętla obiektu, jak zwykle, dlatego nie mogę sprawdzić, czy klucz jest funkcją i go zignorować.

Pytanie

Czy jest jakiś sposób na nawiasowanie natywnych komunikatów o błędach JSON.stringify? Jeśli nie, dlaczego tak się dzieje?

Metody obejścia tego

  • Trzymaj się prostych komunikatów o błędach opartych na łańcuchach lub twórz osobiste obiekty błędów i nie polegaj na rodzimym obiekcie Error.
  • Właściwości ciągnięcia: JSON.stringify({ message: error.message, stack: error.stack })

Aktualizacje

@Ray Toal Sugerowane w komentarzu, że patrzę na deskryptory właściwości . Teraz jest jasne, dlaczego to nie działa:

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

Wynik:

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 }

Klucz: enumerable: false.

Zaakceptowana odpowiedź stanowi obejście tego problemu.

JayQuerie.com
źródło
3
Czy sprawdziłeś deskryptory właściwości dla właściwości w obiekcie błędu?
Ray Toal
3
Pytanie było dla mnie „dlaczego” i znalazłem odpowiedź na dole pytania. Nie ma nic złego w opublikowaniu odpowiedzi na swoje pytanie i prawdopodobnie zyskasz w ten sposób większe uznanie. :-)
Michael Scheper,

Odpowiedzi:

178

Możesz zdefiniować a, Error.prototype.toJSONaby pobrać równinę Objectreprezentującą 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"}

Używanie Object.defineProperty()dodaje, toJSONale enumerablesama nie jest własnością.


Jeśli chodzi o modyfikację Error.prototype, chociaż toJSON()nie można jej zdefiniować Errorkonkretnie, metoda jest nadal standaryzowana dla obiektów w ogóle (patrz: krok 3). Zatem ryzyko kolizji lub konfliktów jest minimalne.

Choć nadal go całkowicie uniknąć, JSON.stringify()jest replacerparametr może być stosowany zamiast:

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
źródło
3
Jeśli użyjesz .getOwnPropertyNames()zamiast .keys(), otrzymasz właściwości niepoliczalne, bez konieczności ich ręcznego definiowania.
8
Lepiej nie dodawać do Error.prototype, może powodować problemy, gdy w przyszłej wersji JavaScrip Error.prototype faktycznie ma funkcję toJSON.
Jos de Jong,
3
Ostrożny! To rozwiązanie przerywa obsługę błędów w natywnym sterowniku mongodb węzła: jira.mongodb.org/browse/NODE-554
Sebastian Nowak
5
W przypadku, gdy ktoś zwraca uwagę na swoje błędy linkera i konflikty nazw: jeśli używasz opcji zamiennika, powinieneś wybrać inną nazwę parametru dla keyin, function replaceErrors(key, value)aby uniknąć konfliktu nazw .forEach(function (key) { .. }); replaceErrors keyparametr jest używany w tej odpowiedzi.
404 Nie znaleziono
2
Zacienianie keyw tym przykładzie, choć dozwolone, jest potencjalnie mylące, ponieważ pozostawia wątpliwości, czy autor zamierzał odwoływać się do zmiennej zewnętrznej, czy nie. propNamebyłby bardziej wyrazistym wyborem dla wewnętrznej pętli. (BTW, myślę, że @ 404NotFound oznaczało linter (narzędzie do analizy statycznej), a nie „linker” ) W każdym razie użycie replacerfunkcji niestandardowej jest doskonałym rozwiązaniem, ponieważ rozwiązuje problem w jednym, odpowiednim miejscu i nie zmienia natywnego / globalne zachowanie.
iX3
261
JSON.stringify(err, Object.getOwnPropertyNames(err))

wydaje się działać

[ z komentarza / u / ub3rgeek do / r / javascript ] i komentarza felixfbecker poniżej

laggingreflex
źródło
57
Czesanie odpowiedzi,JSON.stringify(err, Object.getOwnPropertyNames(err))
felixfbecker
5
Działa to dobrze dla rodzimego obiektu błędu ExpressJS, ale nie działa z błędem Mongoose. Błędy Mongoose mają zagnieżdżone obiekty dla ValidationErrortypów. Nie spowoduje to strunowania zagnieżdżonego errorsobiektu w obiekcie typu błąd Mongoose ValidationError.
steampowered
4
taka powinna być odpowiedź, ponieważ jest to najprostszy sposób, aby to zrobić.
Huan,
7
@felixfbecker To szuka nazw nieruchomości tylko jeden poziom głębokości . Jeśli masz var spam = { a: 1, b: { b: 2, b2: 3} };i biegniesz Object.getOwnPropertyNames(spam), dostaniesz ["a", "b"]- oszukańcze, ponieważ bobiekt ma swój własny b. Można dostać zarówno w stringify rozmowy, ale chcesz przegapićspam.b.b2 . To źle.
ruffin
1
@ruffin to prawda, ale może być nawet pożądane. Myślę, że OP chciał tylko upewnić się messagei stacksą zawarte w JSON.
felixfbecker
74

Ponieważ nikt nie mówi o części dlaczego , odpowiem na to pytanie.

Dlaczego JSON.stringifyzwraca pusty obiekt?

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

Odpowiedź

Z dokumentu JSON.stringify () ,

Dla wszystkich innych instancji Object (w tym Map, Set, WeakMap i WeakSet), tylko ich wyliczalne właściwości będą serializowane.

a Errorobiekt nie ma swoich wyliczalnych właściwości, dlatego drukuje pusty obiekt.

Sanghyun Lee
źródło
4
Dziwne, nikt nawet się nie przejmował. Tak długo, jak działa poprawka, zakładam :)
Ilya Chernomordik
1
Pierwsza część tej odpowiedzi jest nieprawidłowa. Istnieje sposób użycia JSON.stringifyprzy użyciu jego replacerparametru.
Todd Chaffee
1
@ToddChaffee to dobra uwaga. Naprawiłem swoją odpowiedź. Sprawdź to i zachęcamy do poprawy. Dzięki.
Sanghyun Lee
52

Modyfikacja świetnej odpowiedzi Jonathana, aby uniknąć łatania małp:

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
źródło
3
Pierwszy raz słyszę monkey patching:)
Chris Prince,
2
@ChrisPrince Ale to nie będzie ostatni raz, szczególnie w JavaScript! Oto Wikipedia na temat łatania małp , tylko dla informacji przyszłych ludzi. (W odpowiedzi Jonathana , jak Chris rozumie, jesteś dodanie nowej funkcji toJSON, bezpośrednio do Error„s prototypu , który często nie jest to świetny pomysł. Może ktoś ma inny już, która w tym czeki, ale wtedy nie wiem co tak jak robi to inna wersja. Lub jeśli ktoś niespodziewanie dostanie twoją lub przyjmie, że prototyp Error ma określone właściwości, wszystko może się popsuć.)
ruffin
jest to miłe, ale pomija stos błędu (pokazany w konsoli). nie jestem pewien szczegółów, jeśli jest to związane z Vue lub co innego, chciałem tylko o tym wspomnieć.
phil294
23

Jest to świetny pakiet dla node.js, że: serialize-error.

Dobrze radzi sobie nawet z zagnieżdżonymi obiektami Error, czego tak naprawdę potrzebowałem w swoim projekcie.

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

Łukasz Czerwiński
źródło
Nie, ale można to przełożyć. Zobacz ten komentarz .
iX3
To jest poprawna odpowiedź. Błędy serializacji nie są trywialnym problemem, a autor biblioteki (doskonały programista z wieloma bardzo popularnymi pakietami) dołożył znacznych starań, aby obsłużyć przypadki krawędzi, jak widać w pliku README: „Własne właściwości są zachowane. Nieobliczalne właściwości są niepoliczalne (nazwa, komunikat, stos). Właściwości policzalne są niepoliczalne (wszystkie właściwości oprócz tych niepoliczalnych). Obsługiwane są odwołania cykliczne. "
Dan Dascalescu
9

Możesz także przedefiniować te właściwości, których nie można wyliczyć, aby były wyliczalne.

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

a może stackteż nieruchomości.

cheolgook
źródło
9
Nie zmieniaj obiektów, których nie posiadasz, może to zniszczyć inne części aplikacji i powodzenia w znalezieniu przyczyny.
fregante
7

Musieliśmy serializować dowolną hierarchię obiektów, w której pierwiastek główny lub dowolna z zagnieżdżonych właściwości w hierarchii może być wystąpieniem błędu.

Nasze rozwiązanie było użyć replacerparam się JSON.stringify(), np:

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
źródło
5

Żadna z powyższych odpowiedzi nie wydawała się poprawnie serializować właściwości, które znajdują się w prototypie błędu (ponieważ getOwnPropertyNames()nie obejmuje właściwości dziedziczonych). Nie byłem również w stanie przedefiniować właściwości, tak jak w jednej z sugerowanych odpowiedzi.

Oto rozwiązanie, które wymyśliłem - używa lodash, ale można go zastąpić ogólnymi wersjami tych funkcji.

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

Oto test, który zrobiłem w 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
źródło
2

Pracowałem nad formatem JSON dla programów dołączających dzienniki i skończyłem tutaj, próbując rozwiązać podobny problem. Po chwili zdałem sobie sprawę, że mogę po prostu zmusić Node do wykonania pracy:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}
Jason
źródło
1
Powinno być instanceofi nie instanceOf.
lakshman.pasala