Promise Chaining and exit strategy

Issue

Currently when coupon code is invalid I am getting response as invalid code with 400, invalidInput. But it also throwing log as:
"crash Cannot set headers after they are sent to the client"

After debugging I realized my return is not exiting the chain and its going to next block of chain.

Why return statement does not exit the entire chain here?

Is it correct implementation of promise chaining with exit strategy?

MasterOrder.findOne({
    where: {
        fk_user_id: uid,
        id: orderId,
        status: 0,
    }
}).then(function(orderObj) {  
    if (!orderObj) {
        return res.fail("Invalid Order ID", 400, "invalidInput");
    }

    return HUSListing.findByPk(orderObj.fk_listing_id);
}).then(function(listingDetails) {  
    if (!listingDetails) {
        return res.fail("Invalid Order ID With Listing", 400, "invalidInput");
    }
    
    return isValidBooking({at: req.body.time, listingId: req.body.listing_id});

}).then(function(isValidBookingResult) {
    if (!isValidBookingResult) {
        return res.fail("The requested not available", 400, "invalidInput");
    }

   return db.sequelize.query(
        `SELECT user.firstName, user.lastName, 
            FROM HUSListing`);

}).then(function(HUSResultData) {
    if (HUSResultData.length == 0) {
        return res.fail("Listing not found", 400, "invalidInput");
    }

    let couponCodeObj = {
        couponCode: couponCode,
        uid: uid
    }

    return validateCoupon(couponCodeObj);
}).then(function(couponData) {

    if (!couponData.isApplicable) {
        return res.fail(couponData.message, 400, "invalidInput");
    }
    console.log("COMMING HERE");
    couponID = couponData.data.id;
    
    return upsertUserCoupon({
        fk_user_id: uid,
        fk_coupon_id: couponData.id,
        isUsed: 1,
        fk_masterOrder_id: orderId
    }, {
        fk_masterOrder_id: orderId,
    });
}).then(function(r) {

    if (!r || !r.id) { 
        console.log("COMMING HERE 1");
        return res.fail("FAILED TO upsertUserCoupon", 500, "internalServerError");
    }

    return upsertMasterOrder({
        taxAmount: taxAmount,
        payableAmount: payableAmount,
        payableAmountInCent: payableAmount * 100,
        fk_coupon_id: couponID,
        discountedPrice: discountedPrice,
    }, {
        id: orderId
    });
}).then(function(upsertMasterOrderResult) {

    if (upsertMasterOrderResult && upsertMasterOrderResult.id > 0) {

        console.log("COMMING HERE 2");
        return res.respondCreated(upsertMasterOrderResult);
    } else {
        console.log("COMMING HERE 3");
        return res.fail("FAILED TO APPLY COUPON CODE", 500, "internalServerError");
    }
}).catch(err => {
    console.log("crash", err.message);
    return res.fail(err.message, 500, "internalServerError");
});

Solution

return in a .then() handler (as long as you’re not returning a rejected promise) just goes to the next .then() handler in the chain. That’s how promise chains work. To abort the rest of the chain, you can throw and it will then go to the next .catch() in the chain.

There is no way to abort all of the following .then() and .catch() handlers in the chain other than setting a flag somewhere and checking that flag in all subsequent handlers to tell them to skip their work.

Aborting or stopping the control flow is a lot easier when using await instead of .then() because return will indeed stop further processing in your control flow. Here’s how this would look using await:

try {
    const orderObj = await MasterOrder.findOne({
        where: {
            fk_user_id: uid,
            id: orderId,
            status: 0,
        }
    });
    if (!orderObj) {
        return res.fail("Invalid Order ID", 400, "invalidInput");
    }
    const listingDetails = await HUSListing.findByPk(orderObj.fk_listing_id);
    if (!listingDetails) {
        return res.fail("Invalid Order ID With Listing", 400, "invalidInput");
    }
    const isValidBookingResult = await isValidBooking({ at: req.body.time, listingId: req.body.listing_id });
    if (!isValidBookingResult) {
        return res.fail("The requested not available", 400, "invalidInput");
    }
    const HUSResultData = await db.sequelize.query(`SELECT user.firstName, user.lastName, FROM HUSListing`);
    if (HUSResultData.length == 0) {
        return res.fail("Listing not found", 400, "invalidInput");
    }
    let couponCodeObj = {
        couponCode: couponCode,
        uid: uid
    }

    const couponData = await validateCoupon(couponCodeObj);
    if (!couponData.isApplicable) {
        return res.fail(couponData.message, 400, "invalidInput");
    }
    couponID = couponData.data.id;

    const r = await upsertUserCoupon({
        fk_user_id: uid,
        fk_coupon_id: couponData.id,
        isUsed: 1,
        fk_masterOrder_id: orderId
    }, {
        fk_masterOrder_id: orderId,
    });
    if (!r || !r.id) {
        return res.fail("FAILED TO upsertUserCoupon", 500, "internalServerError");
    }
    const upsertMasterOrderResult = await upsertMasterOrder({
        taxAmount: taxAmount,
        payableAmount: payableAmount,
        payableAmountInCent: payableAmount * 100,
        fk_coupon_id: couponID,
        discountedPrice: discountedPrice,
    }, {
        id: orderId
    });
    if (upsertMasterOrderResult && upsertMasterOrderResult.id > 0) {
        await res.respondCreated(upsertMasterOrderResult);
    } else {
        return res.fail("FAILED TO APPLY COUPON CODE", 500, "internalServerError");
    }
} catch (e) {
    console.log("crash", err.message);
    return res.fail(err.message, 500, "internalServerError");
}

You could also stay with the .then() structure and throw errors rather than call res.fail() so all errors get caught and processed in the .catch() like this. The throw will reject the promise chain, causing it to advance to the next .catch() skipping all intervening .then() handlers. You can then "handle" the error in that .catch():

class myError extends Error() {
    constructor(msg, code, tag) {
        super(msg)
        this.code = code;
        this.tag = tag;
    }
}

MasterOrder.findOne({
    where: {
        fk_user_id: uid,
        id: orderId,
        status: 0,
    }
}).then(function(orderObj) {  
    if (!orderObj) {
        throw new MyError("Invalid Order ID", 400, "invalidInput");
    }

    return HUSListing.findByPk(orderObj.fk_listing_id);
}).then(function(listingDetails) {  
    if (!listingDetails) {
        throw new MyError("Invalid Order ID With Listing", 400, "invalidInput");
    }
    
    return isValidBooking({at: req.body.time, listingId: req.body.listing_id});

}).then(function(isValidBookingResult) {
    if (!isValidBookingResult) {
        throw new MyError("The requested not available", 400, "invalidInput");
    }

   return db.sequelize.query(
        `SELECT user.firstName, user.lastName, 
            FROM HUSListing`);

}).then(function(HUSResultData) {
    if (HUSResultData.length == 0) {
        throw new MyError("Listing not found", 400, "invalidInput");
    }

    let couponCodeObj = {
        couponCode: couponCode,
        uid: uid
    }

    return validateCoupon(couponCodeObj);
}).then(function(couponData) {

    if (!couponData.isApplicable) {
        throw new MyError(couponData.message, 400, "invalidInput");
    }
    console.log("COMMING HERE");
    couponID = couponData.data.id;
    
    return upsertUserCoupon({
        fk_user_id: uid,
        fk_coupon_id: couponData.id,
        isUsed: 1,
        fk_masterOrder_id: orderId
    }, {
        fk_masterOrder_id: orderId,
    });
}).then(function(r) {

    if (!r || !r.id) { 
        console.log("COMMING HERE 1");
        throw new MyError("FAILED TO upsertUserCoupon", 500, "internalServerError");
    }

    return upsertMasterOrder({
        taxAmount: taxAmount,
        payableAmount: payableAmount,
        payableAmountInCent: payableAmount * 100,
        fk_coupon_id: couponID,
        discountedPrice: discountedPrice,
    }, {
        id: orderId
    });
}).then(function(upsertMasterOrderResult) {

    if (upsertMasterOrderResult && upsertMasterOrderResult.id > 0) {

        console.log("COMMING HERE 2");
        return res.respondCreated(upsertMasterOrderResult);
    } else {
        console.log("COMMING HERE 3");
        throw new MyError("FAILED TO APPLY COUPON CODE", 500, "internalServerError");
    }
}).catch(err => {
    if (err instanceof MyError) {
        res.fail(err.message, err.code, err.tag);
    } else {
        console.log("unexpected error", err.message);
        res.fail(err.message, 500, "internalServerError");
    }
});

Answered By – jfriend00

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published