Search code examples
javascriptnode.jspromisees6-promise

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

Solution

  • 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