Uncaught exceptions when passing promises into pg promise batch transaction

111 views Asked by At

I'm working on learning express and pg promise and have come across an issue that I suspect is a result from my not as firm as it should be grasp of promises.

Basically I have written some functions that I can use to query the database. I also have a function to use when handling any rejections. Those functions work perfectly when they are used alone (whether they succeed or throw an exception).

Here is a sample example:

const addToColumn = (tableName, columnName, entryId, amountToAdd) => {
    return db.one('UPDATE ${table:name} SET ${column:name} = ${column:name} + ${amount:csv} WHERE id = ${id:csv} RETURNING *', {
        table: tableName,
        column: columnName,
        amount: amountToAdd,
        id: entryId
    }).catch(err => handleQueryErr(err));
};

The functions also work perfectly when passed into a batch transaction manually like this:

const transferEnvelopeBudgetByIds =  async (req, res, next) => {
    try{
        req.updatedEnvelopes = await db.tx(t => {
            return t.batch([
                addToColumn('envelopes', 'budget', req.envelopeFromId, -req.transferBudget),
                addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget)
            ]);
        }).catch(err => handleTransactionErr(err));
        next();
    }catch(err){
        next(err);
    }
};

But then when I try to call this function in the middleware instead, if there is any database error (such as the database not running), then the server crashes with an uncaught excemption:

const batchQuery = (queryArray) => {
    return db.tx(t => {
        return t.batch(queryArray);
    }).catch(err => handleTransactionErr(err));
};


const transferEnvelopeBudgetByIds =  async (req, res, next) => {
    try{
        req.updatedEnvelopes = await batchQuery([
            addToColumn('envelopes', 'budget', req.envelopeToId, -req.transferBudget),
            addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget)
        ])
        next();
    }catch(err){
        next(err);
    }
};

From what I can tell the rejection never get's caught anywhere and has something to do with passing the array of promises into the function. If the query succeeds then the batchQuery function works. It only fails when it fails to catch a database error. Any ideas? If it's not possible to work I can just write out the whole batchQuery every time I need it, but I would still like to understand why it isn't working.

Thanks in advance!

Edit: Here it is with the callback, it's now catching all errors as expected and works perfectly if everything succeeds. It also catches the error if one of the queries fails, but unfortunately it doesn't rollback when using the batchQuery function. I thought batch was supposed to take an array of promises to resolve, but the current method doesn't seem to quite work as I anticipated.

const transferEnvelopeBudgetByIds =  async (req, res, next) => {
    try{
    req.updatedEnvelopes = await batchQuery(() => 
        [
            addToColumn('envelopes', 'budget', req.envelopeFromId, -req.transferBudget),
            addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget)
        ]);  
        next();
    }catch(err){
        next(err);
    }
};

Edit 2: I have it working now using functions, but not quite with the batchQuery function. I now pass t as an optional argument into addToColumn and it works great.

const addToColumn = (tableName, columnName, entryId, amountToAdd, t) => {
    let context = t;
    if(context === undefined){
        context = db;
    }
    return context.one('UPDATE ${table:name} SET ${column:name} = ${column:name} + ${amount:csv} WHERE id = ${id:csv} RETURNING *', {
        table: tableName,
        column: columnName,
        amount: amountToAdd,
        id: entryId
    }).catch(err => handleQueryErr(err));
};

const transferEnvelopeBudgetByIds = async (req, res, next) => {
    try{
        req.updatedEnvelopes = await db.tx(t => {
            return t.batch([
                addToColumn('envelopes', 'budget', req.envelopeFromId, -req.transferBudget, t),
                addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget, t)
            ])
        }).catch(err => handleTransactionErr(err));
        next();
    }catch(err){
        next(err);
    }
};

2

There are 2 answers

1
DanE On

This should prevent the server from crashing due to an uncaught exception

const transferEnvelopeBudgetByIds = async (req, res, next) => {
try {
    req.test = await batchQuery([
        addToColumn('envelopes', 'budget', 100, req.toId).catch(err => handleQueryErr(err)),
        addToColumn('envelopes', 'budget', 100, req.fromId).catch(err => handleQueryErr(err))
    ])
    next();
} catch (err) {
    next(err);
}};
1
vitaly-t On

Your error-handling is all over the place, hence unpredictable transaction outcome. You need to streamline it, move error handling into one place...

function addToColumn(tableName, columnName, entryId, amountToAdd, t) {
    return (t || db).one('UPDATE ${table:name} SET ${column:name} = ${column:name} + ${amount:csv} WHERE id = ${id:csv} RETURNING *', {
        table: tableName,
        column: columnName,
        amount: amountToAdd,
        id: entryId
    });
};

async function transferEnvelopeBudgetByIds(req, res, next) {
    try {
        const res = await db.tx(async t => {
            const one = await addToColumn('envelopes', 'budget', req.envelopeFromId, -req.transferBudget, t);
            const two = await addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget, t);
            return {one, two};
        });
        // res = {one, two};
        next();
    } catch(err) {
        next(err);
    }
};

Also, batch is now obsolete, you do not need to use it at all.