
本文详解 pg-promise 批量数据库操作中因 promise 传递不当导致的未捕获异常问题,指出 `t.batch()` 已废弃,并提供基于显式 `await` 和统一错误处理的现代事务写法。
在使用 pg-promise 构建事务性数据库操作时,一个常见陷阱是:将已执行(即已返回 Promise 实例)的查询函数直接传入 t.batch(),而非在事务上下文内按需构造 Promise。这会导致错误无法被事务层捕获,进而引发 Uncaught Exception——尤其在数据库连接失败、SQL 语法错误或约束冲突等场景下,Node.js 进程可能意外崩溃。
根本原因在于:
- addToColumn(...) 若不接收事务对象 t,默认使用全局 db 实例执行,其返回的 Promise 脱离事务上下文;
- 当该 Promise 被提前调用(如 batchQuery([...]) 中直接传入 addToColumn(...) 调用结果),它会在 db.tx() 启动前就已开始执行,甚至可能在事务开启失败后仍尝试连接数据库;
- 此时 .catch() 仅作用于 db.tx() 返回的 Promise,而内部“游离”的 Promise 抛出的错误无人监听,最终成为未捕获异常。
✅ 正确做法:所有数据库操作必须在事务回调函数内、通过事务对象 t 执行,并避免过早求值。推荐使用 async/await 显式控制流程,而非依赖已废弃的 t.batch():
// ✅ 推荐:参数化 + 可选事务上下文
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,自动回滚 + 统一错误传播
const transferEnvelopeBudgetByIds = async (req, res, next) => {
try {
const result = await db.tx(async 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 错误、约束冲突)均由此处统一捕获
// pg-promise 自动回滚事务,无需手动处理
next(err);
}
};⚠️ 注意事项:
- 不要使用 t.batch():官方文档明确标注其为 obsolete,且语义模糊(易误解为“并发执行”,实则顺序 resolve);现代写法应使用 await 链式调用,语义清晰、调试友好、错误可追溯。
- 禁止提前执行查询:切勿在 db.tx() 外调用 addToColumn(...) 并将返回的 Promise 塞入数组——这等于绕过事务控制。
- 错误处理集中化:事务内的 try/catch 或顶层 catch() 已足够;每个查询函数内部 .catch()(如原代码中的 handleQueryErr)反而会吞掉关键错误,破坏事务原子性。
- 事务对象 t 是必需的:确保所有参与事务的查询都显式传入 t,否则它们运行在独立连接上,既不共享事务隔离级别,也无法联动回滚。
总结:pg-promise 的事务可靠性取决于 Promise 的构造时机与执行上下文。坚持“事务内构造、事务内执行、顶层捕获”的三原则,即可彻底规避未捕获异常,并获得强一致的 ACID 保障。










