Promise.race搭配异步回调数组与超时并行执行的异常排查
问题描述
关闭Web应用前需执行一组时长各异的回调函数,同时设置4000ms超时机制,超时后直接关闭应用避免阻塞。但当前实现中,即便存在未完成的回调(最长回调耗时5000ms),Promise.race仍直接返回Promise.all(callbacks),未按预期触发超时。
当前实现代码
public close() { const callbacks = this.onBeforeCloseCallbacks.map((cb) => new Promise(res => res(cb()))); const timeout = new Promise((res) => setTimeout(res, TIMEOUT_DURATION)); await Promise.race([Promise.all(callbacks), timeout]).then((value) => { // Currently returns Promise.all(callbacks) right away console.log(value) }); await this.pluginEngine.close(); } }
测试用例
it('Should still close the plugin when timing out', async () => { // Arrange const cleanupMock = jest.fn(); const cb1 = jest.fn().mockReturnValue(async () => new Promise(resolve => setTimeout(() => resolve(console.log('cb1')), 3000))); const cleanupMock2 = jest.fn(); const cb2 = jest.fn().mockReturnValue(async () => new Promise(resolve => setTimeout(() => resolve(console.log('cb2')), 11000))); const placementCloseService = new PlacementCloseService(integrationMock, pluginInterface); // Act // onBeforeClose is registering callbacks that needs to be run before close placementCloseService.onBeforeClose(cb1); placementCloseService.onBeforeClose(cb2); await placementCloseService.close(); // Assert expect(cleanupMock).toBeCalled(); expect(cleanupMock2).not.toBeCalled(); expect(pluginInterface.context.close).toBeCalled(); });
问题根源
- 回调Promise包装错误:
new Promise(res => res(cb()))会直接将cb()的返回值(即便它是Promise)作为结果resolve,不会等待回调内部的异步操作完成。这导致所有包装后的Promise瞬间完成,Promise.all(callbacks)立刻resolve,Promise.race自然优先返回它,轮不到超时触发。 - 超时Promise无中断语义:你的超时Promise只是静默resolve,没有任何提示或中断逻辑,即便后续修复了回调包装,也无法区分是回调全部完成还是超时触发。
修复方案
正确包装回调,等待异步操作完成
将回调包装改为真正等待其内部异步逻辑结束:
// 用async/await直接等待回调执行 const callbacks = this.onBeforeCloseCallbacks.map(async (cb) => await cb()); // 或者用Promise.resolve包装 const callbacks = this.onBeforeCloseCallbacks.map(cb => Promise.resolve(cb()));
让超时Promise触发reject,实现超时逻辑
修改超时Promise为reject,通过try/catch捕获超时,直接执行关闭逻辑:
public async close() { const callbacks = this.onBeforeCloseCallbacks.map(cb => Promise.resolve(cb())); const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Close operation timed out")), TIMEOUT_DURATION) ); try { await Promise.race([Promise.all(callbacks), timeout]); } catch (err) { // 超时或回调出错,直接忽略,继续执行关闭 console.log("Proceeding to close after timeout or callback failure"); } await this.pluginEngine.close(); }
补充:取消未完成的回调(可选)
JavaScript原生Promise不支持取消,如果需要在超时后终止未完成的回调,可以给回调添加AbortController令牌,让回调内部监听取消信号:
// 注册回调时传入控制器 onBeforeClose((signal) => new Promise(resolve => { const timer = setTimeout(resolve, 5000); signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }); })); // close方法中创建控制器 public async close() { const controller = new AbortController(); const callbacks = this.onBeforeCloseCallbacks.map(cb => Promise.resolve(cb(controller.signal))); const timeout = new Promise((_, reject) => setTimeout(() => { controller.abort(); reject(new Error("Close timed out")); }, TIMEOUT_DURATION) ); try { await Promise.race([Promise.all(callbacks), timeout]); } catch (err) {} await this.pluginEngine.close(); }
内容的提问来源于stack exchange,提问作者deathknight256
相关产品推荐
相关产品推荐

