เป็นไปไม่ได้ที่จะทำให้เกิดข้อผิดพลาดโดยใช้ JSON.stringify?


330

สร้างปัญหาขึ้นอีกครั้ง

ฉันพบปัญหาเมื่อพยายามส่งข้อความแสดงข้อผิดพลาดโดยใช้ซ็อกเก็ตเว็บ ฉันสามารถทำซ้ำปัญหาที่ฉันเผชิญโดยใช้JSON.stringifyเพื่อรองรับกลุ่มเป้าหมายที่กว้างขึ้น:

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

ปัญหาคือฉันท้ายด้วยวัตถุที่ว่างเปล่า

สิ่งที่ฉันได้ลอง

เบราว์เซอร์

ฉันก่อนพยายามออก node.js และเรียกใช้ในเบราว์เซอร์ต่างๆ Chrome รุ่น 28 ให้ผลลัพธ์แบบเดียวกันและน่าสนใจอย่างน้อย Firefox ก็พยายามทำ แต่ก็ทิ้งข้อความไว้:

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

ฟังก์ชั่นทดแทน

จากนั้นผมก็มองไปที่Error.prototype มันแสดงให้เห็นว่าต้นแบบมีวิธีการเช่นtoStringและtoSource เมื่อรู้ว่าฟังก์ชั่นไม่สามารถทำให้เป็นสตริงได้ฉันได้รวมฟังก์ชั่น replacer ไว้เมื่อเรียก JSON.stringify เพื่อลบฟังก์ชั่นทั้งหมด แต่แล้วก็รู้ว่ามันมีพฤติกรรมแปลก ๆ เช่นกัน:

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

ดูเหมือนจะไม่วนลูปมากกว่าวัตถุตามปกติและดังนั้นฉันไม่สามารถตรวจสอบว่ากุญแจเป็นหน้าที่และไม่สนใจมัน

คำถาม

มีวิธีใดที่จะทำให้ข้อความแสดงข้อผิดพลาดดั้งเดิมเน่าJSON.stringifyหรือไม่ ถ้าไม่เป็นเช่นนั้นทำไมพฤติกรรมนี้จึงเกิดขึ้น

วิธีการรับรอบนี้

  • ติดกับข้อความผิดพลาดแบบสตริงหรือสร้างวัตถุข้อผิดพลาดส่วนตัวและไม่ต้องพึ่งพาวัตถุข้อผิดพลาดดั้งเดิม
  • ดึงคุณสมบัติ: JSON.stringify({ message: error.message, stack: error.stack })

อัพเดท

@Ray กโตอัลแนะนำในความคิดเห็นที่ฉันจะดูที่การอธิบายคุณสมบัติ เป็นที่ชัดเจนแล้วว่าทำไมมันไม่ทำงาน:

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

เอาท์พุท:

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 }

รหัส: enumerable: false.

คำตอบที่ยอมรับให้วิธีแก้ปัญหาสำหรับปัญหานี้


3
คุณได้ตรวจสอบตัวบอกคุณสมบัติสำหรับคุณสมบัติในวัตถุข้อผิดพลาดหรือไม่?
Ray Toal

3
คำถามสำหรับฉันคือ 'ทำไม' และฉันพบว่าคำตอบอยู่ที่ด้านล่างของคำถาม ไม่มีอะไรผิดปกติกับการโพสต์คำตอบสำหรับคำถามของคุณเองและคุณอาจได้รับเครดิตมากขึ้น :-)
Michael Scheper

คำตอบ:


178

คุณสามารถกำหนด a Error.prototype.toJSONเพื่อดึงข้อมูลธรรมดาที่Objectแสดงถึง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"}

ใช้Object.defineProperty()เพิ่มtoJSONโดยไม่ต้องมันเป็นenumerableคุณสมบัติของตัวเอง


เกี่ยวกับการปรับเปลี่ยนError.prototypeแม้ว่าtoJSON()จะไม่ได้กำหนดไว้Errorเป็นพิเศษ แต่วิธีการยังคงเป็นมาตรฐานสำหรับวัตถุทั่วไป (อ้างอิง: ขั้นตอนที่ 3) ดังนั้นความเสี่ยงของการชนหรือความขัดแย้งจึงน้อยมาก

แม้ว่าจะยังคงหลีกเลี่ยงได้อย่างสมบูรณ์JSON.stringify()ของreplacerพารามิเตอร์ที่สามารถนำมาใช้แทน:

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

3
หากคุณใช้.getOwnPropertyNames()แทนคุณ.keys()จะได้รับคุณสมบัติที่ไม่นับโดยไม่ต้องกำหนดด้วยตนเอง

8
ดีกว่าไม่เพิ่มเข้าไปใน Error.prototype สามารถให้ปัญหาเมื่อใน JavaScrip ในอนาคตรุ่น Error.prototype จริง ๆ มีฟังก์ชัน toJSON
Jos de Jong

3
ระวัง! การแก้ปัญหานี้แบ่งการจัดการข้อผิดพลาดในไดรเวอร์ mongodb โหนดเนทิฟ: jira.mongodb.org/browse/NODE-554
เซบาสเตียนโนวัก

5
ในกรณีที่ทุกคนให้ความสนใจกับข้อผิดพลาด linker ของพวกเขาและความขัดแย้งการตั้งชื่อ: หากใช้ตัวเลือก replacer คุณควรเลือกชื่อพารามิเตอร์ที่แตกต่างกันสำหรับkeyในfunction replaceErrors(key, value)เพื่อหลีกเลี่ยงการตั้งชื่อความขัดแย้งกับ.forEach(function (key) { .. }); replaceErrors keyพารามิเตอร์ที่ไม่ได้ใช้ในคำตอบนี้
404 ไม่พบ

2
เงาของkeyตัวอย่างนี้อาจทำให้เกิดความสับสนเนื่องจากอาจสงสัยว่าผู้เขียนตั้งใจอ้างถึงตัวแปรภายนอกหรือไม่ propNameจะเป็นตัวเลือกที่ชัดเจนสำหรับวงใน (BTW ฉันคิดว่า @ 404NotFound หมายถึง"linter" (เครื่องมือวิเคราะห์แบบคงที่) ไม่ใช่"linker" ) ไม่ว่าในกรณีใดการใช้replacerฟังก์ชั่นที่กำหนดเองเป็นวิธีแก้ปัญหาที่ยอดเยี่ยมในการแก้ปัญหาในสถานที่ที่เหมาะสม / พฤติกรรมทั่วโลก
iX3

261
JSON.stringify(err, Object.getOwnPropertyNames(err))

ดูเหมือนว่าจะทำงาน

[ จากความคิดเห็นโดย / u / ub3rgeek เมื่อ / r / javascript ] และความคิดเห็น felixfbecker ของด้านล่าง


57
JSON.stringify(err, Object.getOwnPropertyNames(err))
ผสมผสาน

5
สิ่งนี้ทำงานได้ดีสำหรับวัตถุ ExpressJS Error แบบเนทีฟ แต่จะไม่ทำงานกับข้อผิดพลาดของ Mongoose ข้อผิดพลาดพังพอนมีวัตถุที่ซ้อนกันสำหรับValidationErrorประเภท นี้จะไม่ stringify ซ้อนวัตถุที่อยู่ในวัตถุข้อผิดพลาดพังพอนประเภทerrors ValidationError
steampowered

4
นี่ควรเป็นคำตอบเพราะมันเป็นวิธีที่ง่ายที่สุดในการทำสิ่งนี้
Huan

7
@felixfbecker ที่มองหาชื่ออสังหาริมทรัพย์เพียงระดับเดียวเท่านั้น ถ้าคุณมีvar spam = { a: 1, b: { b: 2, b2: 3} };และเรียกObject.getOwnPropertyNames(spam)คุณจะได้รับ["a", "b"]- หลอกลวงที่นี่เพราะวัตถุที่มีเป็นของตัวเองb bคุณจะได้รับทั้งในโทร stringify ของคุณ แต่คุณจะพลาด spam.b.b2เลวร้าย.
ruffin

1
@ รัฟฟินมันเป็นความจริง แต่มันอาจจะเป็นที่น่าพอใจ ฉันคิดว่าสิ่งที่ OP ต้องการเพียงเพื่อให้แน่ใจmessageและstackรวมอยู่ใน JSON
felixfbecker

74

ในขณะที่ไม่มีใครพูดถึงเหตุผลว่าทำไมส่วนฉันจะตอบมัน

เหตุใดจึงJSON.stringifyส่งคืนวัตถุเปล่า

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

ตอบ

จากเอกสารของJSON.stringify () ,

สำหรับอินสแตนซ์ของวัตถุอื่น ๆ ทั้งหมด (รวมถึงแผนที่ตั้งค่า WeakMap และ WeakSet) เฉพาะคุณสมบัติที่นับได้เท่านั้นที่จะถูกทำให้เป็นอนุกรม

และErrorวัตถุไม่มีคุณสมบัติที่นับได้นั่นคือสาเหตุที่มันพิมพ์วัตถุเปล่า


4
แปลกไม่มีใครใส่ใจแม้แต่ ตราบใดที่การทำงานของการแก้ไขฉันถือว่า :)
Ilya Chernomordik

1
ส่วนแรกของคำตอบนี้ไม่ถูกต้อง มีวิธีใช้JSON.stringifyโดยใช้replacerพารามิเตอร์เป็น
ทอดด์ Chaffee

1
@ToddChaffee นั่นเป็นจุดที่ดี ฉันแก้ไขคำตอบของฉันแล้ว โปรดตรวจสอบและรู้สึกอิสระที่จะปรับปรุง ขอบคุณ
Lee

52

การแก้ไขคำตอบที่ยอดเยี่ยมของ Jonathan เพื่อหลีกเลี่ยงการปะแก้ลิง:

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

3
ครั้งแรกที่ฉันได้ยินmonkey patching:)
Chris Prince

2
@ChrisPrince แต่มันจะไม่ใช่ครั้งสุดท้ายโดยใช้ JavaScript! นี่คือ Wikipedia เกี่ยวกับMonkey Patchingสำหรับข้อมูลของคนในอนาคต (ในคำตอบของโจนาธาน , คริสเข้าใจคุณกำลังเพิ่มฟังก์ชั่นใหม่toJSON, โดยตรงกับErrorต้นแบบ 'sซึ่งมักจะไม่ได้เป็นความคิดที่ดี. อาจจะมีคนอื่นที่มีอยู่แล้วซึ่งการตรวจสอบนี้ แต่แล้วคุณไม่ทราบว่า ว่ารุ่นอื่น ๆ ไม่หรือถ้ามีคนไม่คาดคิดได้รับความนับถือหรือถือว่าต้นแบบข้อผิดพลาดมีคุณสมบัติเฉพาะสิ่งที่จะทำได้บอร์ก)..
รัฟฟิน

นี่เป็นสิ่งที่ดี แต่จะไม่ใช้สแต็กของข้อผิดพลาด (ซึ่งแสดงในคอนโซล) ไม่แน่ใจในรายละเอียดถ้าเป็น Vue หรืออะไรแค่อยากจะพูดถึงนี้
phil294

23

มีแพคเกจที่ดีสำหรับ Node.js serialize-errorที่เป็น:

มันจัดการกับวัตถุข้อผิดพลาดที่ซ้อนกันดีสิ่งที่ฉันจริง ๆ แล้วฉันต้องการมากในโครงการของฉัน

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


ไม่ได้ แต่มันสามารถทำการ transpiled ได้ ดูความคิดเห็นนี้
iX3

นี่คือคำตอบที่ถูกต้อง Serializing ข้อผิดพลาดไม่เป็นปัญหาเล็ก ๆ น้อย ๆ และผู้เขียนของห้องสมุด (เป็น dev ดีกับแพคเกจที่นิยมอย่างสูงอีกหลายคน) ไปยาวอย่างมีนัยสำคัญกับกรณีจับขอบที่สามารถเห็นได้ใน README: "คุณสมบัติแบบกำหนดเองจะถูกเก็บไว้ไม่นับ. คุณสมบัติจะถูกเก็บไว้ไม่นับ (ชื่อ, ข้อความ, สแต็ค) คุณสมบัติที่นับได้จะถูกเก็บไว้นับไม่ได้ (คุณสมบัติทั้งหมดนอกเหนือจากที่ไม่นับได้) การจัดการแบบวงกลมอ้างอิง "
Dan Dascalescu

9

นอกจากนี้คุณยังสามารถกำหนดคุณสมบัติที่ไม่นับได้อีกครั้งเพื่อให้นับได้

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

และอาจstackทรัพย์สินเช่นกัน


9
อย่าเปลี่ยนวัตถุที่คุณไม่ได้เป็นเจ้าของมันสามารถทำลายส่วนอื่น ๆ ของแอปพลิเคชันของคุณและโชคดีในการค้นหาสาเหตุ
Fregante

7

เราจำเป็นต้องทำให้ลำดับชั้นของวัตถุโดยพลการซึ่งรากหรือคุณสมบัติที่ซ้อนกันในลำดับชั้นอาจเป็นกรณีของข้อผิดพลาด

วิธีการแก้ปัญหาของเราคือการใช้replacerพารามิเตอร์ของJSON.stringify()เช่น:

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


5

ไม่มีคำตอบข้างต้นที่ดูเหมือนจะเป็นอนุกรมคุณสมบัติอย่างถูกต้องซึ่งอยู่ในต้นแบบของข้อผิดพลาด (เพราะgetOwnPropertyNames()ไม่รวมถึงคุณสมบัติที่สืบทอดมา) ฉันยังไม่สามารถกำหนดคุณสมบัติเหมือนหนึ่งในคำตอบที่แนะนำ

นี่เป็นวิธีแก้ปัญหาที่ฉันนำมาใช้ - มันใช้ lodash แต่คุณสามารถแทนที่ lodash ด้วยฟังก์ชันทั่วไปเหล่านั้นได้

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

นี่คือการทดสอบที่ฉันทำใน 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}}}  

2

ฉันทำงานในรูปแบบ JSON สำหรับตัวบันทึกการทำงานและสิ้นสุดที่นี่พยายามแก้ปัญหาที่คล้ายกัน ฉันรู้ว่าฉันสามารถทำให้โหนดทำงานได้:

const util = require("util");
...
return JSON.stringify(obj, (name, value) => {
    if (value instanceof Error) {
        return util.format(value);
    } else {
        return value;
    }
}

1
มันควรจะเป็นและไม่ได้instanceof instanceOf
lakshman.pasala
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.