Node.js Serverless 中的异步任务:为什么必须返回并等待 Promise
Node.js Serverless 中的异步任务:为什么必须返回并等待 Promise
本文讨论一个常见问题:路由处理函数调用了异步任务,但任务没有完成就已经返回响应。在 Serverless 环境中,响应返回后运行时可能被冻结或回收,因此“已经放进定时器”不等于“任务一定会执行完成”。
一、问题示例
下面的代码虽然把路由函数声明成了 async,但 track() 没有返回 Promise:
javascript
复制代码
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
router.get('/track', async (req, res) => {
track(req.query);
res.type('image/png').send('1');
});
function track(query) {
setTimeout(() => {
// 执行统计或其他异步任务
}, 2000);
}
这里有两个问题:
track()返回的是undefined,路由无法等待它。setTimeout()只是把回调放入未来的任务队列,并不代表回调已经完成。
在普通的常驻 Node.js 进程中,定时器通常还有机会执行;但在 Serverless 中,平台可能在响应发送后暂停或回收当前实例,后台任务就可能丢失。
另外,JavaScript 的函数名是 setTimeout,不是 setTimeOut。大小写错误会直接导致 ReferenceError。
二、让异步任务返回 Promise
如果这个任务必须在响应前完成,可以把它封装成 Promise,并在路由中使用 await:
javascript
复制代码
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
function track(query) {
return new Promise((resolve, reject) => {
setTimeout(async () => {
try {
// 这里执行真正的异步操作
// await saveTrackRecord(query);
resolve();
} catch (error) {
reject(error);
}
}, 2000);
});
}
router.get('/track', async (req, res, next) => {
try {
await track(req.query);
res.type('image/png').send('1');
} catch (error) {
next(error);
}
});
这样调用关系就完整了:
text
复制代码
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
HTTP 请求
-> 路由处理函数
-> await track()
-> Promise 完成
-> 返回 HTTP 响应
三、如果底层 API 本身已经返回 Promise
不需要再手动套一层 Promise。直接返回或等待原始 Promise 即可:
javascript
复制代码
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
async function track(query) {
await saveTrackRecord(query);
}
router.get('/track', async (req, res, next) => {
try {
await track(req.query);
res.type('image/png').send('1');
} catch (error) {
next(error);
}
});
手动创建 Promise 时,最容易出现的问题是忘记调用 resolve() 或 reject(),导致请求一直等待,最终触发平台超时。
四、什么时候不应该阻塞响应
如果 track() 只是访问统计、日志记录或消息上报,通常不应该为了它让用户等待两秒。更合理的方案是:
- 把事件写入消息队列。
- 写入可靠的数据库或缓存队列。
- 使用云函数平台提供的异步任务机制。
- 让消费者进程负责重试和失败记录。
例如,路由只负责把事件写入队列:
javascript
复制代码
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
router.get('/track', async (req, res, next) => {
try {
await eventQueue.publish({
type: 'track',
payload: req.query,
});
res.type('image/png').send('1');
} catch (error) {
next(error);
}
});
这比在请求中固定等待两秒更适合 Serverless。关键不是“必须 await 所有任务”,而是要明确任务的可靠性要求:必须完成的任务要等待,可延迟的任务要交给可靠队列。
五、测试异步行为
可以用一个 Promise 测试函数确认任务确实在响应前完成:
javascript
复制代码
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-chevron-left md-editor-icon"><circle cx="12" cy="12" r="10"/><path d="m14 16-4-4 4-4"/></svg>
async function demo() {
const start = Date.now();
await track({ id: 1 });
console.log(`耗时:${Date.now() - start}ms`);
}
demo();
如果 track() 没有返回 Promise,demo() 会几乎立即结束;如果返回了 Promise,耗时会接近定时器设置的时间。
总结
async不会自动等待函数内部启动的所有任务。- 需要等待的异步操作必须返回 Promise,并由调用方
await。 setTimeout只是延迟执行,不提供任务持久化能力。- Serverless 中不要依赖响应返回后的“后台继续执行”。
- 统计、日志等非关键任务应优先使用队列或平台异步机制。
原文中的主要问题是 track() 没有返回 Promise、setTimeout 拼写错误,以及没有区分“必须完成”和“可以异步”的任务。