Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Promise.catch(): how to identify the differences between operational rejects and programmatical throws

After much Googling, I cannot find a clear example how to avoid programming every catch to ascertain if a Promise rejection error is programmatic or operational. Compare this to the Node callback pattern of providing callback(error, params...), where operational errors are cleanly provided in the error parameter, and programmatic errors are processed through throw chains.

Please tell me I'm making a noob mistake and there's an easy answer for this I've missed.


EDIT Node v10.0.0 now solves this exact problem by adding error codes.

Thanks to RisingStack for delivering this to my inbox:

https://blog.risingstack.com/node-js-10-lts-feature-breakdown

...and officially but rather terse (as always):

https://nodejs.org/api/errors.html#errors_error_code


Consider a common example:

function logMeIn (email, password, login_token) {
    selectFromDbByEmailAndCheckPwAndReturnId (email, password)
    .then(id => { return updateUserIdLoginToken(id, login_token); })
    .catch(error => {
        // all rejects and throws end up here
        console.log(error);
    })
})

function selectFromDbByEmailAndCheckPwAndReturnId (email, password) {
   return new Promise((resolve, reject) => {
      db.sql.query( /* params */, (error, results) => {
          blarg = 1; // <-- reference error, programmatic
          // do your SELECT * FROM Users where email=? ... etc.
          if (error) {
               return reject(error); // <-- operational sql error
          :
          :
          if (resultsRowsFromQuery.length === 0) {
             // vvvvv operational error: user not found
             return reject(new Error("User not in database"));
          }
          :
          // hash password & salt, etc etc etc ...
          :
          return resolve(resultRowsFromQuery[0].id);
      });
   });
}
// no need to code out updateUserIdLoginToken...

In this example catch will catch the programmatic error and both operational errors, and I have to program catch to determine which. If I wanted to return to the user the fact that their email is not found, I can't just use the message, because I might accidentally return a reference error message. (Awkward!)

However, compare with the the sql.query pattern and it is very clear that the error is operational, because blarg=1 would bubble up to higher levels were it not in a promise.

I see very little documentation on what the reject value should be, and how to differentiate. I've considered using resolve(new Error()) so that my success fulfillment function determines if there was an operational error and .catch is saved for programmatic errors, but that's just silly.

There's a lot of bad info out there because it often references bluebird, Q, A+ and ES6 over the past 7 years... hard to find examples for ES6 Node/7/9 ... [I've even seen links that claim using .then(func A(), func B()).catch() will send the programmatic errors to B and not to catch(). LOL.]

Thoughts?

EDIT #1: Request for promise-free example:

function logMeIn (email, password, login_token) {
  try {
    selectFromDbByEmailAndCheckPwAndReturnId (email, password, (error, id) => {
      if (error) {
        console.log("Operational error:", error)
        return;
      }
      // no error, got id, do next step...
      updateUserIdLoginToken(id, login_token, error => { 
         // do next thing, like return res.render() or something...
      });
    });
  } catch (e) {
    console.error("Programmatic error:", e);
  }
})

function selectFromDbByEmailAndCheckPwAndReturnId (email, password, callback) {
  db.sql.query( /* params */, (error, results) => {
      blarg = 1; // <-- reference error, programmatic
      // do your SELECT * FROM Users where email=? ... etc.
      if (error) {
         return callback(error, null);
      }
      :
      :
      if (resultsRowsFromQuery.length === 0) {
         // vvvvv operational error: user not found
         return callback(new Error("User not in database"), null);
      }
      :
      // hash password & salt, etc etc etc ...
      :
      return callback(null, id);
  });
}
like image 672
PeterT Avatar asked Mar 11 '18 22:03

PeterT


People also ask

What is catch method in Promise?

The catch() method returns a Promise and deals with rejected cases only. It behaves the same as calling Promise. prototype. then(undefined, onRejected) (in fact, calling obj. catch(onRejected) internally calls obj.

Does Try Catch Catch rejected promises?

Normally, such . catch doesn't trigger at all. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it.

What is difference between try catch and then catch?

What are 'try' and 'catch'? try: It is a section where a block of code is defined for tested whether the code generates an unexpected result while executing. catch: It is a section where another block of code is defined, which is executed if any unexpected result generates in the try block.


2 Answers

You expect too much from both node-style and promise based code. Neither kind of asynchronous functions differentiate between the concepts of operational and programmatic errors, you can literally throw/reject anything, that's why you did not find much documentation about it. Both patterns are primitives for asynchronous code flow, nothing more. The node-style version is a bit awkward because that allows for both synchronous and asynchronous errors to be propagated (you need both try-catch, and if(error) to handle all errors). Although they should use only the asynchronous version. Using both "error channels" in a single function is not a feature, it's just misbehaving code.

Neither node-style nor promise based asynchronous code should throw regular synchronous errors. So don't use these two different error propagation channels to differentiate between programmatic and operational errors.

So to answer the question, how do you differentiate between them? Just as you would do with regular synchronous code, you have to introduce your own abstraction:

  • either make every service function return some kind of Result type which would have a field for operational errors (See rust's error handling: https://doc.rust-lang.org/book/first-edition/error-handling.html)
  • or create an OperationalError class, use as many subclasses as you want, and make your top level code differentiate between OperationalError-s and any other kinds of errors. This is what I recommend.
  • or use what your framework provides, although I did not find any good examples for this
like image 157
Tamas Hegedus Avatar answered Sep 18 '22 14:09

Tamas Hegedus


In callback based code, you have to handle with errors yourself and if required throw the error. The async call won't simply throw the error. Now if you want the promise way of implementation, of course the only way is to treat even error as if they are success and then handle it in the "then" chain... not in catch chain. That's the only way to be certain if there is any error. However as you would be aware, in promises, you can resolve only one data not a comma separated list of data. So, you should follow a standard just as traditional callbacks says, first param would be error and following would be responses if any.

As per your example :

function logMeIn (email, password, login_token) {
    selectFromDbByEmailAndCheckPwAndReturnId (email, password)
    .then(response => { 
      if(response.error) {
        // Operational error
      } else {
        // successful response
      }
    })
    .catch(error => {
        // programmatic errors;
        console.log(error);
    })
})

function selectFromDbByEmailAndCheckPwAndReturnId (email, password) {
   return new Promise((resolve, reject) => {
      db.sql.query( /* params */, (error, results) => {
          blarg = 1; // <-- reference error, programmatic
          // do your SELECT * FROM Users where email=? ... etc.
          if (error) {
               return resolve({ error }); // <-- operational sql error
          :
          :
          if (resultsRowsFromQuery.length === 0) {
             // vvvvv operational error: user not found
             return resolve({ error: new Error("User not in database") });
          }
          :
          // hash password & salt, etc etc etc ...
          :
          return resolve({ result: resultRowsFromQuery[0].id });
      });
   });
}
like image 37
binariedMe Avatar answered Sep 21 '22 14:09

binariedMe