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)
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!