ทำไมฉันถึงโยนเข้าไปในตัวจัดการ Promise.catch ไม่ได้?


127

เหตุใดฉันจึงไม่สามารถโยนการErrorเรียกกลับที่จับได้และปล่อยให้กระบวนการจัดการกับข้อผิดพลาดราวกับว่ามันอยู่ในขอบเขตอื่น ๆ

ถ้าฉันไม่ทำconsole.log(err)อะไรจะถูกพิมพ์ออกมาและฉันก็ไม่รู้ว่าเกิดอะไรขึ้น กระบวนการก็จบลง ...

ตัวอย่าง:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

หากการเรียกกลับถูกดำเนินการในเธรดหลักเหตุใดErrorหลุมดำจึงถูกกลืนกิน


11
มันไม่ได้ถูกหลุมดำกลืนกิน มันปฏิเสธคำสัญญาที่.catch(…)กลับมา
Bergi


แทนที่จะ.catch((e) => { throw new Error() })เขียน.catch((e) => { return Promise.reject(new Error()) })หรือเพียงแค่.catch((e) => Promise.reject(new Error()))
chharvey

1
@chharvey ข้อมูลโค้ดทั้งหมดในความคิดเห็นของคุณมีพฤติกรรมที่เหมือนกันทุกประการยกเว้นข้อแรกนั้นชัดเจนที่สุด
СергейГринько

คำตอบ:


157

ดังที่คนอื่น ๆ ได้อธิบายไว้ว่า "หลุมดำ" นั้นเป็นเพราะการขว้างปาเข้าไป.catchในห่วงโซ่ยังคงดำเนินต่อไปด้วยคำสัญญาที่ปฏิเสธและคุณไม่มีทางจับได้อีกต่อไปนำไปสู่ห่วงโซ่ที่ไม่สิ้นสุดซึ่งกลืนข้อผิดพลาด (แย่มาก!)

เพิ่มอีกหนึ่งรายการเพื่อดูว่าเกิดอะไรขึ้น:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

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

เคล็ดลับ

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

.catch(function(err) { setTimeout(function() { throw err; }); });

แม้แต่หมายเลขบรรทัดก็ยังอยู่ได้ดังนั้นลิงก์ในเว็บคอนโซลก็พาฉันไปที่ไฟล์และบรรทัดที่เกิดข้อผิดพลาด (ดั้งเดิม)

ทำไมมันถึงได้ผล

ข้อยกเว้นใด ๆ ในฟังก์ชันที่เรียกว่าเป็นตัวจัดการการปฏิบัติตามสัญญาหรือตัวจัดการการปฏิเสธจะถูกแปลงโดยอัตโนมัติเป็นการปฏิเสธสัญญาที่คุณควรจะกลับมา รหัสสัญญาที่เรียกใช้ฟังก์ชันของคุณจะดูแลสิ่งนี้

ในทางกลับกันฟังก์ชันที่เรียกโดย setTimeout จะทำงานจากสถานะเสถียรของ JavaScript เสมอกล่าวคือทำงานในวงจรใหม่ในลูปเหตุการณ์ของ JavaScript ไม่มีข้อยกเว้นใด ๆ เลยและนำไปไว้ในเว็บคอนโซล เนื่องจากerrเก็บข้อมูลทั้งหมดเกี่ยวกับข้อผิดพลาดรวมถึงสแต็กไฟล์และหมายเลขบรรทัดเดิมจึงยังคงได้รับการรายงานอย่างถูกต้อง


3
จิ๊บเป็นเคล็ดลับที่น่าสนใจช่วยให้ฉันเข้าใจว่าทำไมถึงได้ผล
Brian Keith

8
เกี่ยวกับเคล็ดลับนั้น: คุณขว้างปาเพราะต้องการบันทึกดังนั้นทำไมไม่เพียงแค่เข้าสู่ระบบโดยตรง? เคล็ดลับนี้จะทำให้เกิดข้อผิดพลาดที่ไม่สามารถตรวจจับได้ในเวลาที่ 'สุ่ม' แต่แนวคิดทั้งหมดของข้อยกเว้น (และวิธีที่สัญญาว่าจะจัดการกับพวกเขา) คือทำให้ผู้โทรต้องรับผิดชอบในการจับข้อผิดพลาดและจัดการกับมัน รหัสนี้มีประสิทธิภาพทำให้ผู้โทรไม่สามารถจัดการกับข้อผิดพลาดได้ ทำไมไม่เพียงสร้างฟังก์ชันเพื่อจัดการกับคุณ? function logErrors(e){console.error(e)}แล้วใช้มันเช่นdo1().then(do2).catch(logErrors). ตอบตัวเองดีมาก btw, +1
Stijn de Witt

3
@jib ฉันกำลังเขียน AWS lambda ซึ่งมีคำสัญญามากมายที่เชื่อมโยงกันไม่มากก็น้อยเช่นในกรณีนี้ ในการใช้ประโยชน์จากการเตือนและการแจ้งเตือนของ AWS ในกรณีที่เกิดข้อผิดพลาดฉันจำเป็นต้องทำให้แลมบ์ดาเกิดข้อผิดพลาด (ฉันเดา) เคล็ดลับเป็นวิธีเดียวที่จะได้รับสิ่งนี้หรือไม่?
masciugo

2
@StijndeWitt ในกรณีของฉันฉันพยายามส่งรายละเอียดข้อผิดพลาดไปยังเซิร์ฟเวอร์ของฉันในwindow.onerrorตัวจัดการเหตุการณ์ โดยการทำsetTimeoutเคล็ดลับนี้สามารถทำได้เท่านั้น มิฉะนั้นwindow.onerrorจะไม่ได้ยินเกี่ยวกับข้อผิดพลาดที่เกิดขึ้นใน Promise
hudidit

1
@hudidit ยังดีกว่าconsole.logหรือpostErrorToServerคุณสามารถทำในสิ่งที่ต้องทำ ไม่มีเหตุผลใดที่รหัสที่อยู่ในwindow.onerrorไม่สามารถแยกออกเป็นฟังก์ชันแยกกันและถูกเรียกจาก 2 ที่ มันอาจจะสั้นกว่านั้นsetTimeoutด้วยซ้ำ
Stijn de Witt

46

สิ่งสำคัญที่ต้องทำความเข้าใจที่นี่

  1. ทั้งฟังก์ชันthenและcatchฟังก์ชันจะส่งคืนอ็อบเจ็กต์คำสัญญาใหม่

  2. ไม่ว่าจะขว้างปาหรือปฏิเสธโดยชัดแจ้งจะย้ายสัญญาปัจจุบันไปสู่สถานะที่ถูกปฏิเสธ

  3. เนื่องจากthenและcatchส่งคืนวัตถุสัญญาใหม่พวกเขาสามารถถูกล่ามโซ่ได้

  4. หากคุณโยนหรือปฏิเสธภายในตัวจัดการคำสัญญา ( thenหรือcatch) จะถูกจัดการในตัวจัดการการปฏิเสธถัดไปตามเส้นทางการผูกมัด

  5. ดังที่กล่าวไว้โดย jfriend00 ตัวจัดการthenและcatchตัวจัดการจะไม่ทำงานพร้อมกัน เมื่อตัวจัดการโยนมันจะจบลงทันที ดังนั้นสแต็กจะถูกคลายออกและข้อยกเว้นจะหายไป นั่นคือเหตุผลที่การโยนข้อยกเว้นปฏิเสธคำสัญญาปัจจุบัน


ในกรณีของคุณคุณกำลังปฏิเสธภายในdo1โดยการขว้างปาErrorสิ่งของ ตอนนี้สัญญาปัจจุบันจะอยู่ในสถานะปฏิเสธและการควบคุมจะถูกโอนไปยังตัวจัดการถัดไปซึ่งอยู่thenในกรณีของเรา

เนื่องจากthenตัวจัดการไม่มีตัวจัดการการปฏิเสธจึงdo2ไม่มีการดำเนินการใด ๆ คุณสามารถยืนยันได้โดยใช้console.logข้างใน catchเนื่องจากสัญญาปัจจุบันไม่ได้มีการจัดการปฏิเสธก็จะถูกปฏิเสธโดยมีมูลค่าการปฏิเสธจากสัญญาก่อนหน้านี้และการควบคุมจะถูกโอนไปจัดการต่อไปซึ่งเป็น

ในฐานะที่catchเป็นตัวจัดการการปฏิเสธเมื่อคุณทำconsole.log(err.stack);ภายในคุณจะเห็นการติดตามสแต็กข้อผิดพลาด ตอนนี้คุณกำลังขว้างErrorสิ่งของจากมันดังนั้นคำสัญญาที่ส่งกลับมาcatchจะอยู่ในสถานะปฏิเสธ

เนื่องจากคุณไม่ได้แนบตัวจัดการการปฏิเสธใด ๆ เข้ากับcatchเครื่องคุณจึงไม่สามารถสังเกตการปฏิเสธได้


คุณสามารถแยกโซ่และเข้าใจสิ่งนี้ได้ดีขึ้นเช่นนี้

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

ผลลัพธ์ที่คุณจะได้รับจะเป็นอย่างไร

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

ภายในcatchตัวจัดการ 1 คุณจะได้รับค่าของpromiseวัตถุเป็นปฏิเสธ

ในทำนองเดียวกันสัญญาที่ส่งคืนโดยcatchตัวจัดการ 1 ก็ถูกปฏิเสธด้วยข้อผิดพลาดเดียวกันกับที่promiseถูกปฏิเสธและเรากำลังสังเกตมันในcatchตัวจัดการที่สอง


3
นอกจากนี้ยังอาจคุ้มค่าที่จะเพิ่มว่า.then()ตัวจัดการเป็นแบบ async (สแต็กถูกคลายออกก่อนที่จะถูกดำเนินการ) ดังนั้นข้อยกเว้นภายในจึงต้องเปลี่ยนเป็นการปฏิเสธมิฉะนั้นจะไม่มีตัวจัดการข้อยกเว้นที่จะจับได้
jfriend00

7

ฉันลองsetTimeout()วิธีการตามรายละเอียดข้างต้น ...

.catch(function(err) { setTimeout(function() { throw err; }); });

น่ารำคาญที่ฉันพบว่าสิ่งนี้ไม่สามารถทดสอบได้อย่างสมบูรณ์ เนื่องจากมีข้อผิดพลาดแบบอะซิงโครนัสคุณจึงไม่สามารถรวมไว้ในtry/catchคำสั่งได้เนื่องจากcatchจะหยุดฟังเมื่อเกิดข้อผิดพลาดเวลา

ฉันเปลี่ยนกลับไปใช้เพียงแค่ฟังซึ่งทำงานได้อย่างสมบูรณ์และเนื่องจากเป็นวิธีการใช้งาน JavaScript จึงสามารถทดสอบได้อย่างมาก

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});

3
Jest มีตัวจับเวลาที่น่าจะรับมือกับสถานการณ์นี้ได้
jordanbtucker

2

ตามข้อมูลจำเพาะ (ดู 3.III.d) :

d ถ้าโทรแล้วแสดงข้อยกเว้น e,
  a. หากมีการเรียกใช้คำสั่งแก้ไขหรือปฏิเสธสัญญาให้เพิกเฉย
  ข มิฉะนั้นปฏิเสธสัญญาที่มี e เป็นเหตุผล

นั่นหมายความว่าหากคุณใช้ข้อยกเว้นในthenฟังก์ชันจะถูกจับได้และคำสัญญาของคุณจะถูกปฏิเสธ catchไม่สมเหตุสมผลที่นี่เป็นเพียงทางลัดไป.then(null, function() {})

ฉันเดาว่าคุณต้องการบันทึกการปฏิเสธที่ไม่มีการจัดการในรหัสของคุณ ไลบรารีสัญญาส่วนใหญ่unhandledRejectionเริ่มต้นใช้งาน นี่คือส่วนสำคัญที่เกี่ยวข้องกับการอภิปรายเกี่ยวกับเรื่องนี้


เป็นมูลค่าการกล่าวขวัญว่าunhandledRejectionhook มีไว้สำหรับ JavaScript ฝั่งเซิร์ฟเวอร์ในฝั่งไคลเอ็นต์เบราว์เซอร์ที่แตกต่างกันมีโซลูชันที่แตกต่างกัน เรายังไม่ได้มาตรฐาน แต่มันไปถึงที่นั่นช้า แต่แน่นอน
Benjamin Gruenbaum

1

ฉันรู้ว่ามันช้าไปหน่อย แต่ฉันเจอหัวข้อนี้และไม่มีวิธีแก้ปัญหาใดที่ใช้งานได้ง่ายสำหรับฉันดังนั้นฉันจึงคิดขึ้นมาเอง:

ฉันได้เพิ่มฟังก์ชั่นตัวช่วยเล็ก ๆ น้อย ๆ ซึ่งจะคืนสัญญาเช่นนี้:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

จากนั้นหากฉันมีสถานที่เฉพาะในห่วงโซ่สัญญาของฉันที่ฉันต้องการโยนข้อผิดพลาด (และปฏิเสธคำสัญญา) ฉันก็กลับจากฟังก์ชันข้างต้นพร้อมกับข้อผิดพลาดที่สร้างขึ้นของฉันดังนี้:

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

ด้วยวิธีนี้ฉันสามารถควบคุมการโยนข้อผิดพลาดเพิ่มเติมจากภายในห่วงโซ่สัญญา หากคุณต้องการจัดการข้อผิดพลาดของสัญญา 'ปกติ' ด้วยคุณจะต้องขยายการจับเพื่อจัดการกับข้อผิดพลาด "โยนตัวเอง" แยกกัน

หวังว่านี่จะช่วยได้นี่เป็นคำตอบ stackoverflow แรกของฉัน!


Promise.reject(error)แทนที่จะเป็นnew Promise(function (resolve, reject){ reject(error) })(ซึ่งจะต้องมีคำสั่งส่งคืนอยู่ดี)
Funkodebat

0

ใช่สัญญาว่าจะเกิดข้อผิดพลาดในการกลืนและคุณสามารถจับได้เท่านั้น.catchตามที่อธิบายรายละเอียดเพิ่มเติมในคำตอบอื่น ๆ หากคุณอยู่ใน Node.js และต้องการสร้างthrowลักษณะการทำงานปกติให้พิมพ์การติดตามสแต็กไปที่คอนโซลและออกจากกระบวนการคุณสามารถทำได้

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});

1
ไม่มีที่ไม่เพียงพอในขณะที่คุณจะต้องใส่ที่ในตอนท้ายของทุกห่วงโซ่สัญญาที่คุณมี ค่อนข้างขอเกี่ยวกับunhandledRejectionงาน
Bergi

ใช่นั่นเป็นการสมมติว่าคุณผูกมัดคำสัญญาของคุณดังนั้นทางออกจึงเป็นหน้าที่สุดท้ายและจะไม่ถูกจับหลังจากนั้น เหตุการณ์ที่คุณพูดถึงฉันคิดว่ามันเป็นเฉพาะในกรณีที่ใช้ Bluebird
Jesús Carrera

Bluebird, Q, เมื่อไหร่, คำสัญญาดั้งเดิม, …มันน่าจะกลายเป็นมาตรฐาน
Bergi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.