You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
});

问题根源

  1. 回调Promise包装错误:new Promise(res => res(cb())) 会直接将cb()的返回值(即便它是Promise)作为结果resolve,不会等待回调内部的异步操作完成。这导致所有包装后的Promise瞬间完成,Promise.all(callbacks)立刻resolve,Promise.race自然优先返回它,轮不到超时触发。
  2. 超时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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 11:01:01