Node.js Express 异步操作 PostgreSQL 未处理Promise rejection修复
你遇到的UnhandledPromiseRejection警告,根源不在你封装的数据库查询逻辑,而在Express路由调用时的异常捕获缺失。
你目前封装的db.query本质是直接透传pg库Pool.query方法返回的Promise,当SQL存在语法错误、查询执行失败时,这个Promise会正常触发reject流程;再加上你没有对查询结果做空值校验,当查询不到匹配的用户数据时,访问result.rows[0].id会直接抛出TypeError——如果await这些可能抛出错误的逻辑时没有配套的异常捕获机制,Promise的reject状态没有被任何代码接住,就会触发Node.js的未处理Promise拒绝警告,后续高版本Node.js会直接让进程非零退出。
必须修复的核心逻辑:路由层异常捕获
Express 4.x版本本身不会自动捕获async路由函数内部抛出的Promise错误,只会把reject的Promise直接扔到事件循环中,这是绝大多数人用async/await写Express路由时遇到未处理Promise警告的根本原因。
错误写法示例
// 没有任何异常捕获的async路由,触发警告的典型写法 router.post('/verify', async (req, res) => { // SQL执行错误时,这个Promise的reject没人接住 const result = await db.query('SELECT * FROM users WHERE email = $1 AND password = $2;', [req.body.email, req.body.password]); // 查询无结果时rows[0]为undefined,访问.id会抛出TypeError const userId = result.rows[0].id; res.json({ userId }); })
基础修复写法:全链路try-catch
在路由内用try/catch块包裹所有异步逻辑,把捕获到的错误转发给Express的全局错误处理中间件:
router.post('/verify', async (req, res, next) => { try { const result = await db.query('SELECT * FROM users WHERE email = $1 AND password = $2;', [req.body.email, req.body.password]); // 增加空值校验,避免空结果访问属性报错 if (result.rows.length === 0) { return res.status(401).json({ message: '账号或密码错误' }); } const userId = result.rows[0].id; res.json({ userId }); } catch (err) { // 把错误交给Express错误中间件统一处理 next(err); } })
简化修复写法:通用async路由包装
如果不想每个路由都写重复的try-catch代码,可以写一个通用包装函数,自动接住async路由抛出的Promise错误并转发给错误中间件:
// 通用async路由错误包装器 const asyncHandler = (routeFn) => (req, res, next) => { Promise.resolve(routeFn(req, res, next)).catch(next); } // 业务路由直接用包装器包裹即可,无需手写try-catch router.post('/verify', asyncHandler(async (req, res) => { const result = await db.query('SELECT * FROM users WHERE email = $1 AND password = $2;', [req.body.email, req.body.password]); if (result.rows.length === 0) { return res.status(401).json({ message: '账号或密码错误' }); } const userId = result.rows[0].id; res.json({ userId }); }));
可选优化:增强数据库封装的日志能力
你目前的数据库封装逻辑本身没有问题,可以在不吞掉错误的前提下,增加SQL执行失败的日志打印,方便后续排查问题:
const { Pool } = require('pg') var config = { user: process.env.DB_USER, database: process.env.DB, password: process.env.DB_PASSWORD, host: process.env.DB_HOST, port: process.env.DB_PORT, max: 10, idleTimeoutMillis: 30000, }; const pool = new Pool(config); module.exports = { query: (text, params) => { // 保留原有SQL请求日志 console.log(`sql: ${text}, params: ${params}`); return pool.query(text, params).catch(err => { // 前置打印SQL执行错误日志,不截断错误传递 console.error(`SQL执行失败: ${text}, 参数: ${JSON.stringify(params)}, 错误: ${err.message}`); // 必须把错误重新抛出,让调用端的try-catch能正常捕获,不能吞掉错误 throw err; }); } }
注意:千万不要在数据库封装里捕获错误后不往外抛,这种做法会把SQL错误静默吞掉,导致上层业务逻辑在查询失败时依然继续执行,引发更多不可控的逻辑异常。
内容的提问来源于stack exchange,提问作者fadedbee

