Mongoose Promise链式调用如何提前返回响应终止后续执行
Promise链式调用的默认规则是:只要上一个then回调没有抛出错误,返回值就会被包装成resolved状态的Promise传递到下一个then,无论这个返回值是数据库查询结果还是HTTP响应对象。你在命中materials.includes(mId)条件时返回res.json(),这个返回值会直接流入下一个then的update参数位置,触发第二次响应发送,最终导致「Cannot set headers after they are sent to the client」报错。
方法1:分支内嵌套后续逻辑(原生Promise无额外语法依赖)
不需要走后续更新、发响应的分支直接返回,需要继续执行的逻辑全部放在else分支内部,从结构上避免提前返回的分支流入公共then:const mId = req.body.mId; const cId = req.body.cId; const lId = req.body.lId; LP.findOne({ 'userId': lId, 'courseId': cId }) .then(lP => lP.materials) .then(materials => { if (materials.includes(mId)) { console.log('A') return res.json({'status': 'success'}) } materials.push(mId) // 把更新+后续响应逻辑放在当前分支的Promise链里,不会被提前返回的分支触发 return LP.findOneAndUpdate( { 'userId': lId, 'courseId': cId }, { $set: { materials: materials } }, { new: true } ).then(update => { console.log('B') return res.json({ 'status': 'success' }) }) }) .catch(err => res.json({ 'status': 'fail' }))方法2:抛出标记性错误中断链
提前返回的分支主动抛出一个带特殊标识的错误,跳过所有后续正常业务逻辑的then,在最终的catch里区分是提前结束还是真实业务异常:const mId = req.body.mId; const cId = req.body.cId; const lId = req.body.lId; LP.findOne({ 'userId': lId, 'courseId': cId }) .then(lP => lP.materials) .then(materials => { if (materials.includes(mId)) { console.log('A') // 自定义标记,标识是正常提前结束,不是系统错误 throw { isEarlyTerminate: true } } materials.push(mId) return LP.findOneAndUpdate( { 'userId': lId, 'courseId': cId }, { $set: { materials: materials } }, { new: true } ) }) .then(update => { console.log('B') return res.json({ 'status': 'success' }) }) .catch(err => { if (err?.isEarlyTerminate) { return res.json({'status': 'success'}) } return res.json({ 'status': 'fail' }) })方法3:改用async/await写法(生产环境最推荐)
这是目前Node.js服务端写异步逻辑的首选方案,用同步线性的写法处理异步流程,需要提前终止时直接return即可,完全不存在Promise链式调用的分支穿透问题,代码可读性和可维护性更高:async function addMaterial(req, res) { try { const { mId, cId, lId } = req.body; const lP = await LP.findOne({ 'userId': lId, 'courseId': cId }); const materials = lP.materials; if (materials.includes(mId)) { console.log('A'); return res.json({'status': 'success'}); } materials.push(mId); await LP.findOneAndUpdate( { 'userId': lId, 'courseId': cId }, { $set: { materials: materials } }, { new: true } ); console.log('B'); return res.json({ 'status': 'success' }); } catch (err) { return res.json({ 'status': 'fail' }); } }
额外注意:你贴的原始代码里,更新查询时用的
learnerId、courseId、materialId和前面从req.body里解构的lId、cId、mId变量名不统一,实际运行会报变量未定义错误,调试时记得对齐变量名。
内容的提问来源于stack exchange,提问作者Humayun Afzal

