4

I am struggling to get my head wrapped around what the proper pattern is for handling errors within nested await/async routines, yet keeping the code clean and simple. (despite reading countless articles and blogs)

I have a set of functions that are (fundamentally) similar to the following:

async validate(params) {
    const recCount = await this._getCount(db, params);

    if( recCount > 0 )
        return "Record already exists";
}

_getCount is a wrapper that creates the sql

async _getCount(conn, regdata) {
    const sql = "SELECT count(*) AS 'count' FROM myTable WHERE product = ? and category = ?";
    let rows = await this._execSQL(conn, sql, [ regdata.product, regdata.category ]);
    return rows[0].count;
}

and the actual query is executed as follows:

async _execSQL(conn, sql, data) {
    const [ rows ] = await conn.query(sql, data);
    return rows;
}

The method conn.query (from the mysql2/promise library) will reject the promise if the query fails.

So, my question becomes what is the proper pattern for handling the exceptions?

In a synchronous world, I could nothing to the _execSQL nor the _getCount and just catch the exception in validate; just naturally letting the exception bubble up.

However, in the async world how can I do the equivalent without getting the 'Unhandled Promise' exception?

Am I stuck with having to catch the error at every single async routine all the way through the levels?

Or is there a better way without using something like process.on('unhandledRejection',...) which feels like I am circumventing the problem?

EDIT: Added example and stack trace

Ok, so I have actually added this code to my application and put the try/catch in the validate function. Verbatim code is:

async validate(db, params) {
    let recCount;

    try {
        recCount = await this._getCount(db, params);
    } catch (err) {
        console.log('Caught error', err);
    }

    if (recCount > 0) return 'Record already exists';
}

async _getCount(conn, regdata) {
    const sql = "SELECT count(*) AS 'count' FROM myTable WHERE product = ? and category = ?";
    let rows = await this._execSQL(conn, sql, [ regdata.product, regdata.category ]);
    return rows[0].count;
}

async _execSQL(conn, sql, data) {
    const [ rows ] = await conn.query(sql, data);
    return rows;
}

I have a event handler for unhandledRejection, which reports out the event along with the inner exception along with the stack trace. This is what it dumps out:

Stack Trace:

AppError: Unhandled promise rejection.   Plugin may not be properly handling error.
    at process.on (D:\Development\website\service\server.js:73:5)
    at emitTwo (events.js:126:13)
    at process.emit (events.js:214:7)
    at emitPendingUnhandledRejections (internal/process/promises.js:108:22)
    at process._tickCallback (internal/process/next_tick.js:189:7)

Inner Error:

{   "message": "connect ECONNREFUSED 127.0.0.1:3306",   "code": "ECONNREFUSED",   "errno": "ECONNREFUSED" }

Error: connect ECONNREFUSED 127.0.0.1:3306
    at PromisePool.query (D:\Development\website\webhooks\node_modules\mysql2\promise.js:323:22)
    at Registration._execSQL (D:\Development\website\webhooks\plugins\registration.js:108:31)
    at Registration._logRequest (D:\Development\website\webhooks\plugins\registration.js:179:14)
    at Registration.register (D:\Development\website\webhooks\plugins\registration.js:52:8)
    at Router.exec (D:\Development\website\service\router.js:119:20)
    at IncomingMessage.request.on (D:\Development\website\service\server.js:292:47)
    at emitNone (events.js:106:13)
    at IncomingMessage.emit (events.js:208:7)
    at endReadableNT (_stream_readable.js:1064:12)
    at _combinedTickCallback (internal/process/next_tick.js:138:11)
user3072517
  • 513
  • 1
  • 7
  • 21
  • 1
    In case anyone falls into the same trap that caught me here for some time, I had inadvertently not put an `await` in the prior operation (`_logRequest`) and by not doing that it ran in parallel with the `_getCount`. Both ended up throwing an error, but only one was caught. Putting in this example code helped locate the issue. So....lesson learned....make sure you `await` **all** async functions that you want run in a serial manner. – user3072517 Feb 03 '19 at 07:02

1 Answers1

1

You can always let rejections bubble up and choose the best level to catch them:

async function f1() { return await f2(); }
async function f2() { return await f3(); }
async function f3() {
  return Promise.reject('no way!');
  // or
  throw 'no way!';
}

async function f_await() {
  try {
    console.log('never succeeds here', await f1());
  } catch (err) {
    console.log('I check for errors at the level I prefer!');
    throw 'I can even intercept and rethrow!';
  }
  return 'or i can keep going like eveything is fine';
}
function f_then() {
  f1().then(console.log.bind(null, 'never succeeds here'))
  .catch(function (err) {
    console.log('I check for errors at the level I prefer!');
    throw 'I can even intercept and rethrow!';
  }).then(function () {
    return 'or i can keep going like eveything is fine';
  });
}

If you trigger an unhandled rejection warning, it's because... you didn't handle some rejection at any point in the chain, whereas you always need to: even in synchronous code, if an exception is raised and never caught, the computer will tell you how unhappy it is.

If you think the best way to deal with an SQL query being rejected in your code is in validate, then go for it: surround this await with a try/catch block and "handle" the error in the catch the way you think is best... Not sure I see the problem here!

Stock Overflaw
  • 3,203
  • 1
  • 13
  • 14
  • 2
    Then maybe something else is going on here. If I don't put a try/catch block specifically around the `await conn.query`, I end up getting the `unhandledRejection` error even when I have a try/catch in the `validate` function. – user3072517 Feb 03 '19 at 06:02
  • And the error's stacktrace leads you to `validate` even when you stop the propagation with a `catch`? Seems weird. I'd vote for "something else going on here"! – Stock Overflaw Feb 03 '19 at 06:05
  • Updated the original post with execution example and stack trace. – user3072517 Feb 03 '19 at 06:22
  • 2
    Indeed, there is no mention of `validate`, but there is a `register` method in `webhooks/plugins/registration.js` that calls `_logRequest`, that in turn calls `_execSQL`. All this being triggered by your `Router`, which apparently declares a route that does not... handle rejections. ;) – Stock Overflaw Feb 03 '19 at 06:40
  • 1
    LOL! I just saw that myself. It was definitely a (L)user error! Dang! One of those times you see what you want to see not what is there. Crap! – user3072517 Feb 03 '19 at 06:42
  • That's normal: after one hour on the same bug, one won't see the obvious. :) That being said, even though routes should have their own top-level, fail-safe error-handling logic (each route will behave differently, whether HTML of JSON is expected for instance), the error you're having should be tested at server start-up, so that you cannot have an apparently running server without checking the DB is up and listening with its configured host/port/credentials. Anyway that's what I'd do, it doesn't cost much since it runs only when you launch. (But crashing the DB is good to test the code!) – Stock Overflaw Feb 03 '19 at 06:46
  • Actually, I am...this is a test case that I am running to simulate a downed connection. Anyway. I am marking the above as the answer, because it is....I just didn't see it. – user3072517 Feb 03 '19 at 06:51