การจัดการการจับหลายครั้งในห่วงโซ่สัญญา


125

ฉันยังค่อนข้างใหม่สำหรับสัญญาและกำลังใช้ bluebird อยู่ในขณะนี้ แต่ฉันมีสถานการณ์ที่ฉันไม่แน่ใจว่าจะจัดการกับมันอย่างไรดีที่สุด

ตัวอย่างเช่นฉันมีห่วงโซ่สัญญาภายในแอปด่วนดังนี้:

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

ดังนั้นพฤติกรรมที่ฉันตามคือ:

  • ไปรับบัญชีด้วย Id
  • หากมีการปฏิเสธ ณ จุดนี้ให้ทิ้งระเบิดและส่งคืนข้อผิดพลาด
  • หากไม่มีข้อผิดพลาดในการแปลงเอกสารกลับไปเป็นแบบจำลอง
  • ตรวจสอบรหัสผ่านด้วยเอกสารฐานข้อมูล
  • หากรหัสผ่านไม่ตรงกันให้ทิ้งระเบิดและส่งคืนข้อผิดพลาดอื่น
  • หากไม่มีข้อผิดพลาดให้เปลี่ยนรหัสผ่าน
  • จากนั้นคืนความสำเร็จ
  • หากมีข้อผิดพลาดอื่นให้คืน 500

ดังนั้นการจับในขณะนี้ดูเหมือนจะไม่หยุดการผูกมัดและนั่นก็สมเหตุสมผลแล้วดังนั้นฉันจึงสงสัยว่ามีวิธีใดที่ฉันจะบังคับให้โซ่หยุด ณ จุดใดจุดหนึ่งตามข้อผิดพลาดหรือมีวิธีที่ดีกว่า เพื่อจัดโครงสร้างสิ่งนี้เพื่อให้ได้รูปแบบของพฤติกรรมการแตกแขนงตามที่มีในกรณีif X do Y else Zนี้

ความช่วยเหลือใด ๆ จะดีมาก


คุณสามารถปลูกใหม่หรือกลับก่อนกำหนดได้หรือไม่?
Pieter21

คำตอบ:


126

ลักษณะการทำงานนี้เหมือนกับการโยนซิงโครนัส:

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

นั่นคือครึ่งหนึ่งของ.catch- เพื่อให้สามารถกู้คืนจากข้อผิดพลาดได้ อาจเป็นที่พึงปรารถนาที่จะปลูกใหม่เพื่อส่งสัญญาณว่าสถานะยังคงเป็นข้อผิดพลาด:

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

อย่างไรก็ตามสิ่งนี้จะใช้ไม่ได้กับกรณีของคุณเนื่องจากตัวจัดการในภายหลังจับข้อผิดพลาดได้ ปัญหาที่แท้จริงที่นี่คือตัวจัดการข้อผิดพลาด "HANDLE ANYTHING" โดยทั่วไปเป็นแนวทางปฏิบัติที่ไม่ดีโดยทั่วไปและเป็นปัญหาอย่างมากในภาษาโปรแกรมและระบบนิเวศอื่น ๆ ด้วยเหตุนี้บลูเบิร์ดจึงนำเสนอการจับที่พิมพ์และเพรดิเคต

ข้อดีเพิ่มเติมคือตรรกะทางธุรกิจของคุณไม่ (และไม่ควร) ต้องตระหนักถึงวงจรการร้องขอ / การตอบกลับเลย ไม่ใช่ความรับผิดชอบของแบบสอบถามในการตัดสินใจว่าสถานะ HTTP และข้อผิดพลาดใดที่ไคลเอ็นต์ได้รับและในภายหลังเมื่อแอปของคุณเติบโตขึ้นคุณอาจต้องการแยกตรรกะทางธุรกิจ (วิธีสืบค้นฐานข้อมูลของคุณและวิธีประมวลผลข้อมูลของคุณ) จากสิ่งที่คุณส่งไปยังไคลเอนต์ (รหัสสถานะ http อะไรข้อความอะไรและคำตอบ)

นี่คือวิธีการเขียนโค้ดของคุณ

ก่อนอื่นฉัน.QueryจะโยนNoSuchAccountErrorมันฉันจะย่อยคลาสจากPromise.OperationalErrorที่ Bluebird มีให้อยู่แล้ว หากคุณไม่แน่ใจว่าจะมีข้อผิดพลาดอย่างไรแจ้งให้เราทราบ

นอกจากนี้ฉันยังจัดคลาสย่อยเพื่อAuthenticationErrorทำสิ่งที่ต้องการ:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

อย่างที่คุณเห็น - มันสะอาดมากและคุณสามารถอ่านข้อความเช่นคู่มือการใช้งานของสิ่งที่เกิดขึ้นในกระบวนการ นอกจากนี้ยังแยกออกจากคำขอ / การตอบกลับ

ตอนนี้ฉันจะเรียกมันจากตัวจัดการเส้นทางดังนี้:

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

ด้วยวิธีนี้ตรรกะจะรวมอยู่ในที่เดียวและการตัดสินใจว่าจะจัดการข้อผิดพลาดให้กับลูกค้านั้นรวมอยู่ในที่เดียวและจะไม่เกะกะกันและกัน


11
คุณอาจต้องการเพิ่มว่าสาเหตุของการมี.catch(someSpecificError)ตัวจัดการระดับกลางสำหรับข้อผิดพลาดเฉพาะบางอย่างคือหากคุณต้องการตรวจจับข้อผิดพลาดประเภทใดประเภทหนึ่ง (ซึ่งไม่เป็นอันตราย) ให้จัดการกับมันและดำเนินการตามขั้นตอนต่อไป ตัวอย่างเช่นฉันมีรหัสเริ่มต้นบางอย่างที่มีลำดับของสิ่งที่ต้องทำ สิ่งแรกคือการอ่านไฟล์ config จากดิสก์ แต่ถ้าไฟล์ config นั้นผิดพลาดนั่นคือข้อผิดพลาดตกลง (โปรแกรมสร้างขึ้นเป็นค่าเริ่มต้น) ดังนั้นฉันจึงสามารถจัดการกับข้อผิดพลาดเฉพาะนั้นและดำเนินการต่อส่วนที่เหลือของโฟลว์ นอกจากนี้ยังอาจมีการล้างข้อมูลให้ดีขึ้นเพื่อไม่ให้ออกในภายหลัง
jfriend00

1
ฉันคิดว่า "นั่นเป็นครึ่งหนึ่งของจุด .catch - เพื่อให้สามารถกู้คืนจากข้อผิดพลาดได้" ทำให้ชัดเจน แต่ขอบคุณที่ชี้แจงเพิ่มเติมซึ่งเป็นตัวอย่างที่ดี
Benjamin Gruenbaum

1
จะเกิดอะไรขึ้นหากไม่ได้ใช้ Bluebird? สัญญา es6 ธรรมดามีเฉพาะข้อความแสดงข้อผิดพลาดสตริงที่ถูกส่งไปเพื่อจับ
นาฬิกา

3
@clocksmith กับ ES6 สัญญาว่าคุณติดอยู่กับการจับทุกอย่างและทำinstanceofชิ้นงานด้วยตัวเอง
Benjamin Gruenbaum

1
สำหรับผู้ที่มองหาการอ้างอิงสำหรับข้อผิดพลาด subclassing วัตถุอ่าน bluebirdjs.com/docs/api/catch.html#filtered-catch บทความยังทำซ้ำคำตอบที่จับได้หลายข้อที่ให้ไว้ที่นี่
mummybot

47

.catchทำงานเหมือนtry-catchคำสั่งซึ่งหมายความว่าคุณต้องจับเพียงครั้งเดียวในตอนท้าย:

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });

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

8
@Grofit สำหรับสิ่งที่คุ้มค่า - การจับพิมพ์ใน Bluebird เป็นความคิดของ Petka (Esailija) ที่จะเริ่มต้นด้วย :) ไม่จำเป็นต้องโน้มน้าวเขาว่าเป็นแนวทางที่ดีกว่าที่นี่ ฉันคิดว่าเขาไม่อยากทำให้คุณสับสนเพราะหลาย ๆ คนใน JS ไม่ค่อยเข้าใจแนวคิดนี้
Benjamin Gruenbaum

17

ฉันสงสัยว่าจะมีวิธีบังคับโซ่ให้หยุด ณ จุดใดจุดหนึ่งตามข้อผิดพลาดได้หรือไม่

ไม่ได้คุณไม่สามารถ "สิ้นสุด" โซ่ได้จริงๆเว้นแต่คุณจะโยนข้อยกเว้นที่ทำให้เกิดฟองจนสุด ดูคำตอบของ Benjamin Gruenbaumสำหรับวิธีการทำเช่นนั้น

การสร้างรูปแบบของเขาจะไม่แยกประเภทข้อผิดพลาด แต่ใช้ข้อผิดพลาดที่มีstatusCodeและbodyฟิลด์ที่สามารถส่งจาก.catchตัวจัดการทั่วไปเพียงตัวเดียว วิธีแก้ปัญหาของเขาอาจสะอาดกว่าทั้งนี้ขึ้นอยู่กับโครงสร้างแอปพลิเคชันของคุณ

หรือหากมีวิธีที่ดีกว่าในการจัดโครงสร้างสิ่งนี้เพื่อให้ได้รูปแบบของพฤติกรรมการแตกแขนง

ใช่คุณสามารถจะแยกทางกับสัญญา อย่างไรก็ตามนี่หมายถึงการออกจากโซ่และ "ย้อนกลับ" ไปที่การทำรังเช่นเดียวกับที่คุณทำในคำสั่ง if-else หรือ try-catch ที่ซ้อนกัน:

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});

5

ฉันทำในลักษณะนี้:

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

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

ฟังก์ชันอื่น ๆ ของคุณอาจมีลักษณะดังนี้:

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}

4

อาจจะสายไปหน่อย แต่ก็สามารถทำรังได้.catchดังที่แสดงไว้ที่นี่:

Mozilla Developer Network - การใช้สัญญา

แก้ไข: ฉันส่งสิ่งนี้เนื่องจากมีฟังก์ชันที่ถามโดยทั่วไป อย่างไรก็ตามมันไม่ได้อยู่ในกรณีนี้โดยเฉพาะ เนื่องจากตามที่ผู้อื่นอธิบายโดยละเอียดแล้ว.catchควรจะกู้คืนข้อผิดพลาด คุณไม่สามารถยกตัวอย่างเช่นการส่งคำตอบให้กับลูกค้าในส่วนหลาย .catchเรียกกลับเพราะ.catchไม่มีอย่างชัดเจนreturn แก้ไขมันด้วยundefinedในกรณีที่ก่อให้เกิดการดำเนินการ.thenที่จะทริกเกอร์แม้ว่าห่วงโซ่ของคุณจะไม่ได้รับการแก้ไขจริงๆอาจก่อให้เกิดต่อไปนี้.catchจะเรียกและส่ง การตอบสนองต่อลูกค้าอีกครั้งทำให้เกิดข้อผิดพลาดและมีแนวโน้มที่จะมาUnhandledPromiseRejectionขวางทางคุณ ฉันหวังว่าประโยคที่ซับซ้อนนี้จะมีความหมายสำหรับคุณ


1
@AntonMenshov คุณพูดถูก ฉันขยายคำตอบอธิบายว่าเหตุใดพฤติกรรมที่เขาต้องการจึงยังไม่สามารถทำได้ด้วยการทำรัง
denkquer

2

แทนการที่คุณสามารถทำได้.then().catch()... .then(resolveFunc, rejectFunc)โซ่สัญญานี้จะดีกว่าถ้าคุณจัดการสิ่งต่างๆไปพร้อมกัน นี่คือวิธีที่ฉันจะเขียนใหม่:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

หมายเหตุ:นี่if (error != null)เป็นการแฮ็กเล็กน้อยเพื่อโต้ตอบกับข้อผิดพลาดล่าสุด


1

ฉันคิดว่าคำตอบของ Benjamin Gruenbaum ข้างต้นเป็นทางออกที่ดีที่สุดสำหรับลำดับลอจิกที่ซับซ้อน แต่นี่เป็นทางเลือกของฉันสำหรับสถานการณ์ที่ง่ายกว่า ฉันแค่ใช้errorEncounteredธงร่วมกับreturn Promise.reject()เพื่อข้ามคำสั่งthenหรือcatchข้อความที่ตามมา มันจะมีลักษณะดังนี้:

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

หากคุณมีคู่แล้ว / จับมากกว่าสองคู่คุณควรใช้วิธีแก้ปัญหาของ Benjamin Gruenbaum แต่วิธีนี้ใช้ได้กับการตั้งค่าง่ายๆ

โปรดทราบว่าขั้นสุดท้ายcatchมีเพียงreturn;มากกว่าreturn Promise.reject();เพราะไม่มีสิ่งต่อมาthenที่เราต้องข้ามและจะนับเป็นการปฏิเสธสัญญาที่ไม่มีการจัดการซึ่งโหนดไม่ชอบ ตามที่เขียนไว้ข้างต้นสุดท้ายcatchจะคืนสัญญาที่ได้รับการแก้ไขอย่างสันติ

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