L.P. L.P. - 8 months ago 30
Javascript Question

Promise.all with try/catch simulation

I'm currently running through, and attempting to familiarize myself with promises, and I will cut through the introductory concepts, and get to the meat of the matter. Within NodeJS, using the library BlueBird. I don't want to defer the function calls, I'd also rather not pollute the code more than needed, even though this is introductory coding to get familiar with the premise, as when more advanced concepts are being attempted, I'll get lost on them. I attempted using 'asyn/await' with a try block, but God was that code messy, and didn't work...
More or less guidelines:


  • I do not want to use Promise.settle, I still want to exit when a function rejects, I'd just like to stay within the Catch block if an error is raised.

  • I don't want to use async/await, yield, or generators in some weird way to stop the flow of the code as this is detrimental to the intended practice.

  • If the intended solution to handling this is wrong, please don't just say it's wrong, I understand people's time is valuable, but with limited knowledge of the workings of the language, as much documentation as can be thrown at me, or reasons why I'm wrong are more appreciated than not.

  • I've seen implementations of the concept in CoffeeScript, I don't see a viable reason to move to that syntax system at the moment, as most anything I may use the language for in the coming months would be bare Node back-end management.



Promises contain a catch mechanism built in, which works perfectly, if dealing with a standard single Promise.


// Try/Catch style Promises
funcTwo = function(activate) {
return new Promise(function(resolve, reject) {
var tmpFuncTwo;
if (activate === true) {
tmpFuncTwo = "I'm successful"
resolve(tmpFuncTwo)
} else if (activate === false) {
tmpFuncTwo = "I'm a failure.";
reject(tmpFuncTwo)
} else {
tmpFuncTwo = "Oh this is not good."
throw new Error(tmpFuncTwo);
}
});
}

funcTwo(true)
.then(val => {
console.log("1: ", val)
return funcTwo()
})
.catch(e => {
console.log("2: Err ", e.message)
})





The thing that causes me to be somewhat confused is attempting to uphold the same premise with Promise.all, the error is not handled, as the throw pushes directly to the main Controller.
The exception that is thrown from this snippet never makes it to the the Catch block.



funcThree = function(val) {
return new Promise(function(resolve, reject) {
if (val > 0)
resolve((val + 1) * 5)
else if (val < 0)
reject(val * 2)
else
throw new Error("No work for 0");
})
}
// Output in Dev Console
/*
Extrending to the catch block handling, This will fail, the exception is thrown, and ignores the catch block. Terminating the program.
*/
Promise.all([funcThree(1), funcThree(0), funcThree(-3)])
.then(function(arr) {
for (var ind = 0; ind < arr.length; ind++) {
console.log(arr)
};
}, function(arr) {
console.log(arr)
})
.catch(function(e) {
console.log("Error")
})





I've attempted a simple work around, but I am somewhat new to the language, and am not sure if this is adhering to "Best Practices", as they have been drilled into my mind from Python guidelines.



// Promise all, exceptionHandling
funcThree = (val) => {
return new Promise(function(resolve, reject) {
if (val > 0)
resolve((val + 1) * 5)
else if (val < 0)
reject(val * 2)
else {
var tmp = new Error("No work for 0");
tmp.type = 'CustomError';
reject(tmp);
}
})
}

/*
This works, and doesn't cause any type of mixup
*/
Promise.all([funcThree(1), funcThree(0), funcThree(-3)])
.then(
arr => {
for (var ind = 0; ind < arr.length; ind++) {
console.log(arr)
};
}, rej => {
if (rej.type == 'CustomError')
throw rej;
console.log(arr)
})
.catch(e => {
console.log("Catching Internal ", e.message)
})





This is using the Native Promise library, as well as bluebird

Is there a way to handle this more natively,

In regards to jfriend00's comment. What i mean to say is that I don't want the exception to be handled by anything but the try-catch block. When I attempt to use the same format as for a normal promise, things align perfectly, and my catch is acknowledged, and the error handled. Since Promise.all can only ever resolve/reject I don't think that there is a clean way of delegating the exception that is thrown from the second call to funcTwo in the second code snippet. More or less I'm not sure if what I've done as a workaround," reject, check if reject passed forward an error, and then throw it if it did", is a good solution or if it will cause some deep problem as code expands.

Answer Source

Since Promise.all can only ever resolve/reject I don't think that there is a clean way of delegating the exception that is thrown from the second call to funcTwo in the second code snippet.

In this code block of yours:

// Try/Catch style Promises
funcTwo = function(activate) {
  return new Promise(function(resolve, reject) {
    var tmpFuncTwo;
    if (activate === true) {
      tmpFuncTwo = "I'm successful"
      resolve(tmpFuncTwo)
    } else if (activate === false) {
      tmpFuncTwo = "I'm a failure.";
      reject(tmpFuncTwo)
    } else {
      tmpFuncTwo = "Oh this is not good."
      throw new Error(tmpFuncTwo);
    }
  });
}

There is no difference between a throw and a reject(). The throw is caught by the Promise constructor and turned into a reject(). Personally, I prefer to just use reject() in this case because I think function calls are a bit faster than exceptions.

I don't know if this is codified in a specification or not, but it is generally considered a good idea to reject with an Error object. So, I'd write your code like this:

function funcTwo(activate) {
  return new Promise(function(resolve, reject) {
    if (activate === true) {
      resolve("I'm successful");
    } else {
      let errMsg = activate === false ? "I'm a failure." : "Oh this is not good.";
      reject(new Error(errMsg));
    }
  });
}

Promises either resolve or reject. There is no third error condition that is different than a reject. An exception just becomes a rejection. So, if you have three states to return (like the above code), then you have to just decide how you're going to put the three states into resolve and reject.

Since this is just example code, there is no concrete recommendation here. If activate === false is not actually an error, just a different type of completion that shouldn't abort your other promises in Promise.all(), then you'd want that case to be a resolve(), not a reject(). But, there's no hard and fast rule what is what - it really just depends upon what behavior you want to be natural and simple for the callers so thus it varies from one situation to the next.

In addition, if you don't control the code that is in funcTwo here, then you can just put a .catch() handler on it before you pass it to Promise.all() and you can turn a specific rejection into a resolve if that's how you'd like the Promise.all() logic to work. Promises chain so you can modify their output before passing them on to higher level operations. It is analogous to using try/catch at a lower level to catch and exception and deal with it so higher level code doesn't have to see it (appropriate sometimes).

More or less I'm not sure if what I've done as a workaround," reject, check if reject passed forward an error, and then throw it if it did", is a good solution or if it will cause some deep problem as code expands.

In your Promise.all() code here:

/*
  This works, and doesn't cause any type of mixup
 */
Promise.all([funcThree(1), funcThree(0), funcThree(-3)]).then(arr => {
  for (var ind = 0; ind < arr.length; ind++) {
    console.log(arr[index]);
  }
}, rej => {
  if (rej.type == 'CustomError')
    throw rej;
  console.log(arr)
}).catch(e => {
  console.log("Catching Internal ", e.message)
})

Your first reject handler is not really helping you. It isn't doing anything that you can't just do in your one .catch() handler. Let me repeat this. There are only two outcomes from a promise reject and resolve. There is no third outcome for an exception. Exceptions that occur inside promise callbacks just become rejections. So, your above code can be changed to this:

/*
  This works, and doesn't cause any type of mixup
*/
Promise.all([funcThree(1), funcThree(0), funcThree(-3)]).then(arr => {
  for (var ind = 0; ind < arr.length; ind++) {
    console.log(arr)
  };
}).catch(e => {
  // there is no value for arr here, no result - only a reject reason
  console.log("Catching Internal ", e.message)
  if (rej.type === "CustomError") {
      // do something special for this type of error
  }
  // unless you rethrow here, this rejection will be considered handled
  // and any further chained `.then()` will see the promise as resolved
  // but since there is no return value, the promise will be resolved
  // with an undefined result
});

If you wanted to capture the reject in funcThree() early enough that it could allow the rest of the Promises in Promise.all() to still be tracked and still get their results, then you can either get a Promise.settle() implementation that will follow all promises to their conclusions regardless of how many reject or you can code your own special case:

function funcFour(val) {
    return funcThree(val).catch(err => {
       // catch and examine the error here
       if (err.type === "CustomError") {
           // allow things to continue on here for this specific error
           // and substitute null for the value.  The caller will have
           // to see null as a meaningful result value and separate from
           // a non-null result
           return null;
       } else {
           // Not an error we recognize, stop further processing
           // by letting this promise reject
           throw err;
       }
    });
}

Promise.all([funcFour(1), funcFour(0), funcFour(-3)]).then(arr => {
   // got results, some might be null
   console.log(arr);
}).catch(err => {
   // got some error that made it so we couldn't continue
   console.log(err);
});

I know this is all style opinion, but one of the benefits of good code using promises is that you stop having as deeply indented code and now I see people putting all sorts of extra indentation and lines that simply aren't needed and seem to remove some of the benefits of clean promise coding.

In this case, .then() and .catch() can go on the same line as the promise they follow. And, there's no need to start an inline function definition on a new line.