Apakah tidak mungkin untuk merangkai Kesalahan menggunakan JSON.stringify?

331

Mereproduksi masalah

Saya mengalami masalah ketika mencoba menyampaikan pesan kesalahan menggunakan soket web. Saya dapat meniru masalah yang saya hadapi JSON.stringifyuntuk memenuhi audiensi yang lebih luas:

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

Masalahnya adalah bahwa saya berakhir dengan objek kosong.

Apa yang saya coba

Browser

Saya pertama kali mencoba meninggalkan node.js dan menjalankannya di berbagai browser. Chrome versi 28 memberi saya hasil yang sama, dan cukup menarik, Firefox setidaknya membuat upaya tetapi tidak menerima pesan:

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

Fungsi replacer

Saya kemudian melihat Error.prototype . Ini menunjukkan bahwa prototipe berisi metode seperti toString dan toSource . Mengetahui bahwa fungsi tidak dapat diubah, saya menyertakan fungsi replacer saat memanggil JSON.stringify untuk menghapus semua fungsi, tetapi kemudian menyadari bahwa itu juga memiliki beberapa perilaku aneh:

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

Tampaknya tidak untuk mengulang objek seperti biasanya, dan karena itu saya tidak dapat memeriksa apakah kuncinya adalah fungsi dan mengabaikannya.

Pertanyaan

Apakah ada cara untuk merangkai pesan kesalahan asli dengan JSON.stringify? Jika tidak, mengapa perilaku ini terjadi?

Metode untuk mengatasi ini

  • Tetap dengan pesan kesalahan sederhana berbasis string, atau buat objek kesalahan pribadi dan jangan mengandalkan objek kesalahan asli.
  • Properti tarik: JSON.stringify({ message: error.message, stack: error.stack })

Pembaruan

@ Ray Toal Diusulkan dalam komentar yang saya lihat di deskriptor properti . Jelas sekarang mengapa itu tidak berhasil:

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

Keluaran:

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.

Jawaban yang diterima memberikan solusi untuk masalah ini.

JayQuerie.com
sumber
3
Sudahkah Anda memeriksa deskriptor properti untuk properti di objek kesalahan?
Ray Toal
3
Pertanyaan untuk saya adalah 'mengapa', dan saya menemukan jawabannya ada di bagian bawah pertanyaan. Tidak ada yang salah dengan memposting jawaban untuk pertanyaan Anda sendiri, dan Anda mungkin akan mendapatkan lebih banyak kredibilitas dengan cara itu. :-)
Michael Scheper

Jawaban:

178

Anda dapat menentukan a Error.prototype.toJSONuntuk mengambil suatu dataran yang Objectmewakili 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"}

Menggunakan Object.defineProperty()tambah toJSONtanpa menjadi enumerableproperti itu sendiri.


Mengenai memodifikasi Error.prototype, sementara toJSON()mungkin tidak didefinisikan untuk Errors secara khusus, metode ini masih standar untuk objek secara umum (ref: langkah 3). Jadi, risiko tabrakan atau konflik minimal.

Meskipun, masih menghindarinya sepenuhnya, JSON.stringify()'s replacerparameter dapat digunakan sebagai pengganti:

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
sumber
3
Jika Anda menggunakan .getOwnPropertyNames()alih-alih .keys(), Anda akan mendapatkan properti yang tidak dapat dihitung tanpa harus mendefinisikannya secara manual.
8
Lebih baik tidak menambahkan ke Error.prototype, dapat memberikan masalah ketika dalam versi JavaScrip yang berikutnya Error.prototype sebenarnya memiliki fungsi toJSON.
Jos de Jong
3
Cermat! Solusi ini memecah penanganan kesalahan pada driver node mongodb asli: jira.mongodb.org/browse/NODE-554
Sebastian Nowak
5
Dalam kasus orang memperhatikan kesalahan linker dan konflik penamaan: jika menggunakan opsi pengganti, Anda harus memilih nama parameter yang berbeda untuk keydi function replaceErrors(key, value)untuk menghindari penamaan konflik dengan .forEach(function (key) { .. }); yang replaceErrors keyparameter yang tidak terpakai dalam jawaban ini.
404 Tidak Ditemukan
2
Membayangi keydalam contoh ini, sementara diizinkan, berpotensi membingungkan karena meninggalkan keraguan apakah penulis bermaksud merujuk ke variabel luar atau tidak. propNameakan menjadi pilihan yang lebih ekspresif untuk loop dalam. (BTW, saya pikir @ 404NotFound berarti "linter" (alat analisis statis) bukan "linker" ). Bagaimanapun, menggunakan replacerfungsi kustom adalah solusi yang sangat baik untuk ini karena ini memecahkan masalah dalam satu, tempat yang tepat dan tidak mengubah asli / Perilaku global.
iX3
261
JSON.stringify(err, Object.getOwnPropertyNames(err))

sepertinya berhasil

[ dari komentar oleh / u / ub3rgeek di / r / javascript ] dan komentar felixfbecker di bawah ini

laggingreflex
sumber
57
Menyisir jawabannya,JSON.stringify(err, Object.getOwnPropertyNames(err))
felixfbecker
5
Ini berfungsi dengan baik untuk objek Galat ExpressJS asli, tetapi tidak akan berfungsi dengan kesalahan luwak Kesalahan luwak memiliki objek bersarang untuk ValidationErrorjenis. Ini tidak akan mengikat errorsobjek bersarang di objek tipe kesalahan luwak ValidationError.
steampowered
4
ini harus menjadi jawabannya, karena ini adalah cara paling sederhana untuk melakukan ini.
Huan
7
@ Feliksbecker Itu hanya mencari nama properti sedalam satu tingkat . Jika Anda memiliki var spam = { a: 1, b: { b: 2, b2: 3} };dan menjalankan Object.getOwnPropertyNames(spam), Anda akan mendapatkan ["a", "b"]- menipu di sini, karena bobjek memiliki itu sendiri b. Anda akan mendapatkan keduanya dalam panggilan tegar, tetapi Anda akan kehilanganspam.b.b2 . Itu buruk.
ruffin
1
@ruffin itu benar, tetapi bahkan mungkin diinginkan. Saya pikir apa yang diinginkan OP hanya untuk memastikan messagedan stacktermasuk dalam JSON.
felixfbecker
74

Karena tidak ada yang berbicara tentang bagian mengapa , aku akan menjawabnya.

Mengapa ini JSON.stringifymengembalikan objek kosong?

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

Menjawab

Dari dokumen JSON.stringify () ,

Untuk semua instance Object lainnya (termasuk Map, Set, WeakMap dan WeakSet), hanya properti enumerable mereka yang akan diserialisasi.

dan Errorobjek tidak memiliki properti enumerable, itu sebabnya ia mencetak objek kosong.

Sanghyun Lee
sumber
4
Aneh bahkan tidak ada yang peduli. Selama perbaikan berfungsi saya anggap :)
Ilya Chernomordik
1
Bagian pertama dari jawaban ini salah. Ada cara untuk JSON.stringifymenggunakan replacerparameternya.
Todd Chaffee
1
@ToddChaffee itu poin yang bagus. Saya sudah memperbaiki jawaban saya. Silakan periksa dan jangan ragu untuk memperbaikinya. Terima kasih.
Sanghyun Lee
52

Memodifikasi jawaban hebat Jonathan untuk menghindari tambalan monyet:

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
sumber
3
Pertama kali saya dengar monkey patching:)
Chris Prince
2
@ChrisPrince Tapi ini bukan yang terakhir, khususnya dalam JavaScript! Inilah Wikipedia tentang Monkey Patching , hanya untuk info orang-orang di masa depan. (Dalam jawaban Jonathan , seperti yang dipahami Chris, Anda menambahkan fungsi baru toJSON,, langsung ke Errorprototipe , yang seringkali bukan ide bagus. Mungkin sudah ada orang lain, yang memeriksa ini, tetapi kemudian Anda tidak tahu apa yang versi lain lakukan. Atau jika seseorang secara tak terduga mendapatkan milik Anda, atau menganggap prototipe Error memiliki sifat tertentu, hal-hal bisa bork.)
ruffin
ini bagus, tetapi menghilangkan tumpukan kesalahan (yang ditampilkan di konsol). tidak yakin dengan detailnya, apakah ini terkait Vue atau apa, hanya ingin menyebutkan ini.
phil294
23

Ada paket Node.js besar untuk itu: serialize-error.

Ini menangani dengan baik bahkan objek Kesalahan bersarang, apa yang sebenarnya saya butuhkan dalam proyek saya.

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

Lukasz Czerwinski
sumber
Tidak, tetapi bisa diubah untuk melakukannya. Lihat komentar ini .
iX3
Ini jawaban yang benar. Kesalahan serialisasi bukan masalah sepele, dan penulis perpustakaan (pengembang yang sangat baik dengan banyak paket yang sangat populer) berusaha keras untuk menangani kasus tepi, seperti yang dapat dilihat dalam README: "Properti kustom dipertahankan. Tidak dapat dihitung properti disimpan non-enumerable (nama, pesan, tumpukan). Properti enumerable disimpan enumerable (semua properti selain yang non-enumerable). Referensi melingkar ditangani. "
Dan Dascalescu
9

Anda juga bisa mendefinisikan ulang properti yang tidak dapat dihitung menjadi yang dapat dihitung.

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

dan mungkin stackproperti juga.

Cheolgook
sumber
9
Jangan mengubah objek yang tidak Anda miliki, itu dapat merusak bagian lain dari aplikasi Anda dan semoga berhasil menemukan alasannya.
fregante
7

Kami harus membuat serial hierarki objek yang sewenang-wenang, di mana root atau salah satu properti yang bersarang dalam hierarki dapat menjadi instance dari Kesalahan.

Solusi kami adalah menggunakan replacerparam JSON.stringify(), misalnya:

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

Tak satu pun dari jawaban di atas yang tampaknya membuat cerita bersambung properti dengan benar yang ada pada prototipe Kesalahan (karena getOwnPropertyNames()tidak menyertakan properti yang diwarisi). Saya juga tidak dapat mendefinisikan ulang properti seperti salah satu jawaban yang disarankan.

Ini adalah solusi yang saya buat - menggunakan lodash tetapi Anda dapat mengganti lodash dengan versi generik dari fungsi-fungsi tersebut.

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

Inilah tes yang saya lakukan di 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
sumber
2

Saya sedang mengerjakan format JSON untuk appenders log dan berakhir di sini mencoba untuk memecahkan masalah yang sama. Setelah beberapa saat, saya menyadari bahwa saya hanya bisa membuat Node melakukan pekerjaan:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}
Jason
sumber
1
Seharusnya instanceofdan tidak instanceOf.
lakshman.pasala