Terminating a subprocess ends it abruptly. This prevents rolling back the subprocess' operations and leaves them incomplete.
Ideally subprocesses should end on their own. If that's not possible, graceful termination should be preferred.
The cancelSignal
option can be used to cancel a subprocess. When it is aborted, a SIGTERM
signal is sent to the subprocess.
import {execaNode} from 'execa';
const controller = new AbortController();
const cancelSignal = controller.signal;
setTimeout(() => {
controller.abort();
}, 5000);
try {
await execaNode({cancelSignal})`build.js`;
} catch (error) {
if (error.isCanceled) {
console.error('Canceled by cancelSignal.');
}
throw error;
}
When the gracefulCancel
option is true
, the cancelSignal
option does not send any SIGTERM
. Instead, the subprocess calls getCancelSignal()
to retrieve and handle the AbortSignal
. This allows the subprocess to properly clean up and abort operations.
This option only works with Node.js files.
This is cross-platform. If you do not need to support Windows, signal handlers can also be used.
// main.js
import {execaNode} from 'execa';
const controller = new AbortController();
const cancelSignal = controller.signal;
setTimeout(() => {
controller.abort();
}, 5000);
try {
await execaNode({cancelSignal, gracefulCancel: true})`build.js`;
} catch (error) {
if (error.isGracefullyCanceled) {
console.error('Cancelled gracefully.');
}
throw error;
}
// build.js
import {getCancelSignal} from 'execa';
const cancelSignal = await getCancelSignal();
The AbortSignal
returned by getCancelSignal()
can be passed to most long-running Node.js methods: setTimeout()
, setInterval()
, events, streams, REPL, HTTP/TCP requests or servers, reading / writing / watching files, or spawning another subprocess.
When aborted, those methods throw the Error
instance which was passed to abortController.abort(error)
. Since those methods keep the subprocess alive, aborting them makes the subprocess end on its own.
import {getCancelSignal} from 'execa';
import {watch} from 'node:fs/promises';
const cancelSignal = await getCancelSignal();
try {
for await (const fileChange of watch('./src', {signal: cancelSignal})) {
onFileChange(fileChange);
}
} catch (error) {
if (error.isGracefullyCanceled) {
console.log(error.cause === cancelSignal.reason); // true
}
}
For other kinds of operations, the abort
event should be listened to. Although cancelSignal.addEventListener('abort')
can be used, events.addAbortListener(cancelSignal)
is preferred since it works even if the cancelSignal
is already aborted.
We recommend explicitly stopping each pending operation when the subprocess is aborted. This allows it to end on its own.
import {getCancelSignal} from 'execa';
import {addAbortListener} from 'node:events';
const cancelSignal = await getCancelSignal();
addAbortListener(cancelSignal, async () => {
await cleanup();
process.exitCode = 1;
});
However, if any operation is still ongoing, the subprocess will keep running. It can be forcefully ended using process.exit(exitCode)
instead of process.exitCode
.
If the subprocess is still alive after 5 seconds, it is forcefully terminated with SIGKILL
. This can be configured or disabled using the forceKillAfterDelay
option.
If the subprocess lasts longer than the timeout
option, a SIGTERM
signal is sent to it.
try {
await execa({timeout: 5000})`npm run build`;
} catch (error) {
if (error.timedOut) {
console.error('Timed out.');
}
throw error;
}
To terminate a subprocess when it becomes inactive, the cancelSignal
option can be combined with transforms and some debouncing logic. The following example terminates the subprocess if it has not printed to stdout
/stderr
in the last minute.
import {execa} from 'execa';
import debounceFn from 'debounce-fn';
// 1 minute
const wait = 60_000;
const getInactivityOptions = () => {
const controller = new AbortController();
const cancelSignal = controller.signal;
// Delay and debounce `cancelSignal` each time `controller.abort()` is called
const scheduleAbort = debounceFn(controller.abort.bind(controller), {wait});
const onOutput = {
* transform(data) {
// When anything is printed, debounce `controller.abort()`
scheduleAbort();
// Keep the output as is
yield data;
},
// Debounce even if the output does not include any newline
binary: true,
};
// Start debouncing
scheduleAbort();
return {
cancelSignal,
stdout: onOutput,
stderr: onOutput,
};
};
const options = getInactivityOptions();
await execa(options)`npm run build`;
If the current process exits, the subprocess is automatically terminated unless either:
- The
cleanup
option isfalse
. - The subprocess is run in the background using the
detached
option. - The current process was terminated abruptly, for example, with
SIGKILL
as opposed toSIGTERM
or a successful exit.
subprocess.kill()
sends a signal to the subprocess. This is an inter-process message handled by the OS. Most (but not all) signals terminate the subprocess.
SIGTERM
is the default signal. It terminates the subprocess. On Unix, it can be handled to run some cleanup logic.
const subprocess = execa`npm run build`;
subprocess.kill();
// Is the same as:
subprocess.kill('SIGTERM');
SIGINT
terminates the process. Its handler is triggered on CTRL-C
.
subprocess.kill('SIGINT');
SIGKILL
forcefully terminates the subprocess. It cannot be handled.
subprocess.kill('SIGKILL');
SIGQUIT
terminates the process. On Unix, it creates a core dump.
subprocess.kill('SIGQUIT');
Other signals can be passed as argument. However, most other signals do not fully work on Windows.
The killSignal
option sets the default signal used by subprocess.kill()
and the following options: cancelSignal
, timeout
, maxBuffer
and cleanup
. It is SIGTERM
by default.
const subprocess = execa({killSignal: 'SIGKILL'})`npm run build`;
subprocess.kill(); // Forceful termination
On Unix, most signals (not SIGKILL
) can be intercepted to perform a graceful exit.
process.on('SIGTERM', () => {
cleanup();
process.exit(1);
})
Unfortunately this usually does not work on Windows. The only signal that is somewhat cross-platform is SIGINT
: on Windows, its handler is triggered when the user types CTRL-C
in the terminal. However subprocess.kill('SIGINT')
is only handled on Unix.
Execa provides the gracefulCancel
option as a cross-platform alternative to signal handlers.
When a subprocess was terminated by a signal, error.isTerminated
is true
.
Also, error.signal
and error.signalDescription
indicate the signal's name and human-friendly description. On Windows, those are only set if the current process terminated the subprocess, as opposed to another process.
try {
await execa`npm run build`;
} catch (error) {
if (error.isTerminated) {
console.error(error.signal); // SIGFPE
console.error(error.signalDescription); // 'Floating point arithmetic error'
}
throw error;
}
If the subprocess is terminated but does not exit, SIGKILL
is automatically sent to forcefully terminate it.
The grace period is set by the forceKillAfterDelay
option, which is 5 seconds by default. This feature can be disabled with false
.
The error.isForcefullyTerminated
boolean property can be used to check whether a subprocess was forcefully terminated by the forceKillAfterDelay
option.
This works when the subprocess is terminated by either:
- Calling
subprocess.kill()
with no arguments. - The
cancelSignal
,timeout
,maxBuffer
orcleanup
option.
This does not work when the subprocess is terminated by either:
- Calling
subprocess.kill()
with a specific signal. - Calling
process.kill(subprocess.pid)
. - Sending a termination signal from another process.
Also, this does not work on Windows, because Windows doesn't support signals: SIGKILL
and SIGTERM
both terminate the subprocess immediately. Other packages (such as taskkill
) can be used to achieve fail-safe termination on Windows.
// No forceful termination
const subprocess = execa({forceKillAfterDelay: false})`npm run build`;
subprocess.kill();
subprocess.kill()
only works when the current process terminates the subprocess. To terminate the subprocess from a different process, its subprocess.pid
can be used instead.
const subprocess = execa`npm run build`;
console.log('PID:', subprocess.pid); // PID: 6513
await subprocess;
For example, from a terminal:
$ kill -SIGTERM 6513
Or from a different Node.js process:
import process from 'node:process';
process.kill(subprocessPid);
When terminating a subprocess, it is possible to include an error message and stack trace by using subprocess.kill(error)
. The error
argument will be available at error.cause
.
try {
const subprocess = execa`npm run build`;
setTimeout(() => {
subprocess.kill(new Error('Timed out after 5 seconds.'));
}, 5000);
await subprocess;
} catch (error) {
if (error.isTerminated) {
console.error(error.cause); // new Error('Timed out after 5 seconds.')
console.error(error.cause.stack); // Stack trace from `error.cause`
console.error(error.originalMessage); // 'Timed out after 5 seconds.'
}
throw error;
}