diff --git a/lib/child_process.js b/lib/child_process.js index 0e3e04af0d6..e5ea1954291 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -824,7 +824,8 @@ function spawn(file, args, options) { } }, options.timeout); - child.once('exit', () => { + // 'close' always fires after 'exit', or after 'error' on a spawn failure. + child.once('close', () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; diff --git a/test/parallel/test-child-process-spawn-timeout-clear-on-error.js b/test/parallel/test-child-process-spawn-timeout-clear-on-error.js new file mode 100644 index 00000000000..f674e4ed55f --- /dev/null +++ b/test/parallel/test-child-process-spawn-timeout-clear-on-error.js @@ -0,0 +1,19 @@ +'use strict'; + +// Measures the child's actual exit time, not just its 'error' event. +// The outer spawnSync timeout catches a leaked inner timer. + +const common = require('../common'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); + +const bugStallMs = common.platformTimeout(10000); +const outerTimeoutMs = common.platformTimeout(2000); + +spawnSyncAndExitWithoutError(process.execPath, ['-e', ` + const { spawn } = require('child_process'); + const cp = spawn(process.execPath, ['--version'], { + cwd: '/nonexistent/path/that/should/never/exist', + timeout: ${bugStallMs}, + }); + cp.on('error', () => {}); +`], { timeout: outerTimeoutMs });