Home > Software design >  Promise Chaining and exit strategy
Promise Chaining and exit strategy

Time:08-27

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

CodePudding user response:

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

CodePudding user response:

Return statement promises chain will only call the next chain function with the parameter you tried to return. There is no difference between returning a normal result between return db.sequelize.query and return res.fail, they will just act the same way.

Here is a basic idea of what can be done with that:

class NoopError extends Error {}

function someAsyncMethod1() {
  return Promise.resolve();
}

someAsyncMethod1()
  .then(function (x) {
    return 123;
  })
  .then(function (x) {
    if (x === 123) {
      // no return.
      // res.fail("Invalid Order ID With Listing", 400, "invalidInput");
      // we are leaving to the catch
      throw new NoopError();
    }
    return 345;
  })
  .catch(function (err) {
    if (err instanceof NoopError) {
      console.log("Doing nothing");
    } else {
      console.log("Error we were not expecting but we are ready", err);
      // res.fail(err.message, 500, "internalServerError");
      // Optionally rethrow exception to catch it on upper level, if needed.
      // throw err;
    }
  });

someAsyncMethod1();

I commented res.fail("Invalid Order ID With Listing", 400, "invalidInput"); due to i was doing it in a playground and i had no res, so just uncomment it. Just showing the general idea. Write anything to your res object at any place you want and throw a NoopError after that.

  • Related