From 1e5cc75758893a0009aba31c657ee28dd84c8a9c Mon Sep 17 00:00:00 2001 From: Avocado Date: Wed, 9 Sep 2026 16:19:35 +0900 Subject: [PATCH] child_process: clear timeout timer on spawn error A spawn failure emits 'error' without 'exit', so a timeout timer cleared only on 'exit' stayed armed and kept the event loop alive for the whole timeout. Clear it on 'close', which is emitted in both cases, as exec() already does. Fixes: https://github.com/nodejs/node/issues/65504 Signed-off-by: Avocado --- lib/child_process.js | 2 +- .../test-child-process-spawn-timeout-error.js | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-child-process-spawn-timeout-error.js diff --git a/lib/child_process.js b/lib/child_process.js index 887f5668e37a..4507c131e811 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -824,7 +824,7 @@ function spawn(file, args, options) { } }, options.timeout); - child.once('exit', () => { + child.once('close', () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; diff --git a/test/parallel/test-child-process-spawn-timeout-error.js b/test/parallel/test-child-process-spawn-timeout-error.js new file mode 100644 index 000000000000..b3918a04d320 --- /dev/null +++ b/test/parallel/test-child-process-spawn-timeout-error.js @@ -0,0 +1,31 @@ +'use strict'; + +// Verify the timeout timer is cleared when the child fails to spawn, which +// emits 'error' without 'exit'. + +const { mustCall } = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +function pendingTimers() { + return process.getActiveResourcesInfo() + .filter((type) => type === 'Timeout').length; +} + +assert.strictEqual(pendingTimers(), 0); + +const cp = spawn('this-command-does-not-exist', { + timeout: 6000, +}); + +assert.strictEqual(pendingTimers(), 1); + +cp.on('error', mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); +})); + +cp.on('close', mustCall(() => { + setImmediate(mustCall(() => { + assert.strictEqual(pendingTimers(), 0); + })); +}));