一个异步操作往往同时受两类取消条件约束:自身执行超时,以及进程收到的主动停止请求。与其让业务函数分别监听多个信号,可以在调用边界用 AbortSignal.any() 合并,再把单一 signal 向下传递。
环境:Node.js 20.3+;Linux、macOS 或 Windows;仅使用内置模块;项目使用 ESM。
创建 cancel-demo.mjs:
import { setTimeout as sleep } from 'node:timers/promises';
async function runJob({ signal }) {
for (let step = 1; step <= 10; step += 1) {
await sleep(100, undefined, { signal });
console.log(`step ${step}`);
}
}
const shutdown = new AbortController();
process.once('SIGINT', () => {
shutdown.abort(new Error('shutdown requested'));
});
const signal = AbortSignal.any([
shutdown.signal,
AbortSignal.timeout(250),
]);
try {
await runJob({ signal });
} catch (error) {
if (error.name !== 'AbortError') {
throw error;
}
const reason = signal.reason;
console.error(
'cancelled:',
reason instanceof Error ? `${reason.name}: ${reason.message}` : reason,
);
}
运行:
node cancel-demo.mjs
未进行人工操作时,通常先输出 step 1、step 2,随后输出类似:
cancelled: TimeoutError: The operation was aborted due to timeout
也可以在超时前按 Ctrl+C,此时合并信号保留主动取消的原因,输出 cancelled: Error: shutdown requested。具体完成几个步骤受调度时序影响,不应据此写精确断言。
关键点是让 runJob 只接收外部信号,不在函数内部私自创建计时器;这样调用方可以组合超时、用户取消和服务关闭等策略。node:timers/promises、内置 fetch 等支持 signal 的 API 都可复用同一模式。捕获拒绝时检查的是操作抛出的 AbortError,而区分取消来源时读取合并信号的 reason。