
本文详解 pg-promise 中事务(tx)与异步函数协同使用的常见陷阱,重点解决因 promise 传递不当导致的未捕获异常、事务不回滚等问题,并推荐现代写法替代已废弃的 `t.batch()`。
在使用 pg-promise 构建 Express 应用时,一个高频误区是:将已执行(即已返回 Promise 实例)的查询函数直接传入事务 t.batch(),而非在事务上下文内按需创建 Promise。这会导致两个严重问题:
- 事务上下文丢失:如 addToColumn(...) 在调用时未传入 t,它实际执行于全局 db 实例,脱离事务控制;
- 错误处理断裂:若 batch() 内某个 Promise 被 .catch() 捕获(如你在 addToColumn 中自行 .catch(handleQueryErr)),该错误被“吞掉”,不再向上抛出,导致 t.batch() 无法感知失败,事务不会自动回滚,且外层 try/catch 或 .catch() 也收不到异常——最终表现为 Uncaught Exception 崩溃。
✅ 正确做法:统一错误处理 + 显式事务上下文
首先,移除所有查询函数内部的 .catch()。错误应由事务层或顶层中间件统一捕获,确保原子性与可观测性:
// ✅ 清洁的查询函数:不处理错误,只返回 Promise
const addToColumn = (tableName, columnName, entryId, amountToAdd, t = db) => {
return t.one(
'UPDATE ${table:name} SET ${column:name} = ${column:name} + ${amount:csv} WHERE id = ${id:csv} RETURNING *',
{
table: tableName,
column: columnName,
amount: amountToAdd,
id: entryId,
}
);
};其次,在事务回调中显式 await 每个操作(推荐,更清晰可控),而非依赖 t.batch():
const transferEnvelopeBudgetByIds = async (req, res, next) => {
try {
const result = await db.tx(async t => {
// ✅ 所有操作均绑定到事务对象 `t`
const from = await addToColumn('envelopes', 'budget', req.envelopeFromId, -req.transferBudget, t);
const to = await addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget, t);
return { from, to }; // 可选:返回结果供后续使用
});
req.updatedEnvelopes = result;
next();
} catch (err) {
// ✅ 所有数据库错误(连接失败、SQL 错误、约束冲突等)均在此被捕获
// 事务自动回滚,错误交由 Express 错误中间件处理
next(err);
}
};⚠️ 关于 t.batch() 的重要说明
pg-promise 官方文档明确标注 Task.batch() 已废弃(obsolete)。其设计初衷是并行执行多个独立查询,但:
- 无法保证执行顺序(对有依赖的操作不安全);
- 错误传播机制复杂,易与手动 .catch() 冲突;
- 现代 async/await 已能更优雅地实现串行/并行控制。
如需并行执行(且无依赖),可改用 Promise.all():
await db.tx(async t => {
return Promise.all([
addToColumn('envelopes', 'budget', req.envelopeFromId, -req.transferBudget, t),
addToColumn('envelopes', 'budget', req.envelopeToId, req.transferBudget, t),
]);
});? 提示:Promise.all() 中任一 Promise 拒绝,整个数组即拒绝,事务仍会回滚,符合预期。
? 总结关键原则
- 错误处理集中化:查询函数只负责发起请求,不 .catch();事务或路由层统一捕获并响应。
- 事务上下文显式传递:所有数据库操作必须接收并使用 t(事务对象),杜绝隐式使用 db。
- 避免过时 API:弃用 t.batch(),优先使用 await 串行或 Promise.all() 并行。
-
启用 pg-promise 全局错误监听(可选但推荐):
db.on('error', error => { console.error('pg-promise global error:', error); });作为兜底,捕获极少数逃逸的底层连接异常。
遵循以上模式,即可彻底规避未捕获异常、事务不回滚等顽疾,写出健壮、可维护的数据库事务逻辑。










