execa logo
# 🏁 Termination ## Alternatives 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](#graceful-termination) should be preferred. ## Canceling The [`cancelSignal`](api.md#optionscancelsignal) option can be used to cancel a subprocess. When it is [aborted](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort), a [`SIGTERM` signal](#default-signal) is sent to the subprocess. ```js 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; } ``` ## Graceful termination ### Share a `cancelSignal` When the [`gracefulCancel`](api.md#optionsgracefulcancel) option is `true`, the [`cancelSignal`](api.md#optionscancelsignal) option does not send any [`SIGTERM`](#sigterm). Instead, the subprocess calls [`getCancelSignal()`](api.md#getcancelsignal) to retrieve and handle the [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/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](#handling-signals) can also be used. ```js // 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; } ``` ```js // build.js import {getCancelSignal} from 'execa'; const cancelSignal = await getCancelSignal(); ``` ### Abort operations The [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) returned by [`getCancelSignal()`](api.md#getcancelsignal) can be passed to most long-running Node.js methods: [`setTimeout()`](https://nodejs.org/api/timers.html#timerspromisessettimeoutdelay-value-options), [`setInterval()`](https://nodejs.org/api/timers.html#timerspromisessetintervaldelay-value-options), [events](https://nodejs.org/api/events.html#eventsonemitter-eventname-options), [streams](https://nodejs.org/api/stream.html#new-streamreadableoptions), [REPL](https://nodejs.org/api/readline.html#rlquestionquery-options), HTTP/TCP [requests](https://nodejs.org/api/http.html#httprequesturl-options-callback) or [servers](https://nodejs.org/api/net.html#serverlistenoptions-callback), [reading](https://nodejs.org/api/fs.html#fspromisesreadfilepath-options) / [writing](https://nodejs.org/api/fs.html#fspromiseswritefilefile-data-options) / [watching](https://nodejs.org/api/fs.html#fspromiseswatchfilename-options) files, or spawning another subprocess. When aborted, those methods throw the `Error` instance which was passed to [`abortController.abort(error)`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort). Since those methods keep the subprocess alive, aborting them makes the subprocess end on its own. ```js 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 } } ``` ### Cleanup logic For other kinds of operations, the [`abort`](https://nodejs.org/api/globals.html#event-abort) event should be listened to. Although [`cancelSignal.addEventListener('abort')`](https://nodejs.org/api/events.html#eventtargetaddeventlistenertype-listener-options) can be used, [`events.addAbortListener(cancelSignal)`](https://nodejs.org/api/events.html#eventsaddabortlistenersignal-listener) is preferred since it works even if the `cancelSignal` is already aborted. ### Graceful exit We recommend explicitly [stopping](#abort-operations) each pending operation when the subprocess is aborted. This allows it to end on its own. ```js 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)`](https://nodejs.org/api/process.html#processexitcode) instead of [`process.exitCode`](https://nodejs.org/api/process.html#processexitcode_1). If the subprocess is still alive after 5 seconds, it is forcefully terminated with [`SIGKILL`](#sigkill). This can be [configured or disabled](#forceful-termination) using the [`forceKillAfterDelay`](api.md#optionsforcekillafterdelay) option. ## Timeout ### Execution timeout If the subprocess lasts longer than the [`timeout`](api.md#optionstimeout) option, a [`SIGTERM` signal](#default-signal) is sent to it. ```js try { await execa({timeout: 5000})`npm run build`; } catch (error) { if (error.timedOut) { console.error('Timed out.'); } throw error; } ``` ### Inactivity timeout To terminate a subprocess when it becomes inactive, the [`cancelSignal`](#canceling) option can be combined with [transforms](transform.md) and some [debouncing logic](https://github.com/sindresorhus/debounce-fn). The following example terminates the subprocess if it has not printed to [`stdout`](api.md#resultstdout)/[`stderr`](api.md#resultstderr) in the last minute. ```js 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`; ``` ## Current process exit If the current process exits, the subprocess is automatically [terminated](#default-signal) unless either: - The [`cleanup`](api.md#optionscleanup) option is `false`. - The subprocess is run in the background using the [`detached`](api.md#optionsdetached) option. - The current process was terminated abruptly, for example, with [`SIGKILL`](#sigkill) as opposed to [`SIGTERM`](#sigterm) or a successful exit. ## Signal termination [`subprocess.kill()`](api.md#subprocesskillsignal-error) sends a [signal](https://en.wikipedia.org/wiki/Signal_(IPC)) to the subprocess. This is an inter-process message handled by the OS. Most (but [not all](https://github.com/ehmicky/human-signals#action)) signals terminate the subprocess. [More info.](https://nodejs.org/api/child_process.html#subprocesskillsignal) ### SIGTERM [`SIGTERM`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM) is the default signal. It terminates the subprocess. On Unix, it can [be handled](#handling-signals) to run some cleanup logic. ```js const subprocess = execa`npm run build`; subprocess.kill(); // Is the same as: subprocess.kill('SIGTERM'); ``` ### SIGINT [`SIGINT`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGINT) terminates the process. Its [handler](#handling-signals) is triggered on `CTRL-C`. ```js subprocess.kill('SIGINT'); ``` ### SIGKILL [`SIGKILL`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGKILL) forcefully terminates the subprocess. It [cannot be handled](#handling-signals). ```js subprocess.kill('SIGKILL'); ``` ### SIGQUIT [`SIGQUIT`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGQUIT) terminates the process. On Unix, it creates a [core dump](https://en.wikipedia.org/wiki/Core_dump). ```js subprocess.kill('SIGQUIT'); ``` ### Other signals Other signals can be passed as argument. However, most other signals do not fully [work on Windows](https://github.com/ehmicky/cross-platform-node-guide/blob/main/docs/6_networking_ipc/signals.md#cross-platform-signals). ### Default signal The [`killSignal`](api.md#optionskillsignal) option sets the default signal used by [`subprocess.kill()`](api.md#subprocesskillsignal-error) and the following options: [`cancelSignal`](#canceling), [`timeout`](#timeout), [`maxBuffer`](output.md#big-output) and [`cleanup`](#current-process-exit). It is [`SIGTERM`](#sigterm) by default. ```js const subprocess = execa({killSignal: 'SIGKILL'})`npm run build`; subprocess.kill(); // Forceful termination ``` ### Handling signals On Unix, most signals (not [`SIGKILL`](#sigkill)) can be intercepted to perform a graceful exit. ```js process.on('SIGTERM', () => { cleanup(); process.exit(1); }) ``` Unfortunately this [usually does not work](https://github.com/ehmicky/cross-platform-node-guide/blob/main/docs/6_networking_ipc/signals.md#cross-platform-signals) on Windows. The only signal that is somewhat cross-platform is [`SIGINT`](#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`](#graceful-termination) option as a cross-platform alternative to signal handlers. ### Signal name and description When a subprocess was terminated by a signal, [`error.isTerminated`](api.md#erroristerminated) is `true`. Also, [`error.signal`](api.md#errorsignal) and [`error.signalDescription`](api.md#errorsignaldescription) indicate the signal's name and [human-friendly description](https://github.com/ehmicky/human-signals). On Windows, those are only set if the current process terminated the subprocess, as opposed to [another process](#inter-process-termination). ```js 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; } ``` ## Forceful termination If the subprocess is terminated but does not exit, [`SIGKILL`](#sigkill) is automatically sent to forcefully terminate it. The grace period is set by the [`forceKillAfterDelay`](api.md#optionsforcekillafterdelay) option, which is 5 seconds by default. This feature can be disabled with `false`. The [`error.isForcefullyTerminated`](api.md#errorisforcefullyterminated) 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()`](api.md#subprocesskillsignal-error) with no arguments. - The [`cancelSignal`](#canceling), [`timeout`](#timeout), [`maxBuffer`](output.md#big-output) or [`cleanup`](#current-process-exit) option. This does not work when the subprocess is terminated by either: - Calling [`subprocess.kill()`](api.md#subprocesskillsignal-error) with a specific signal. - Calling [`process.kill(subprocess.pid)`](api.md#subprocesspid). - Sending a termination signal [from another process](#inter-process-termination). Also, this does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events): `SIGKILL` and `SIGTERM` both terminate the subprocess immediately. Other packages (such as [`taskkill`](https://github.com/sindresorhus/taskkill)) can be used to achieve fail-safe termination on Windows. ```js // No forceful termination const subprocess = execa({forceKillAfterDelay: false})`npm run build`; subprocess.kill(); ``` ## Inter-process termination [`subprocess.kill()`](api.md#subprocesskillsignal-error) only works when the current process terminates the subprocess. To terminate the subprocess from a different process, its [`subprocess.pid`](api.md#subprocesspid) can be used instead. ```js const subprocess = execa`npm run build`; console.log('PID:', subprocess.pid); // PID: 6513 await subprocess; ``` For example, from a terminal: ```sh $ kill -SIGTERM 6513 ``` Or from a different Node.js process: ```js import process from 'node:process'; process.kill(subprocessPid); ``` ## Error message and stack trace When terminating a subprocess, it is possible to include an error message and stack trace by using [`subprocess.kill(error)`](api.md#subprocesskillerror). The `error` argument will be available at [`error.cause`](api.md#errorcause). ```js 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; } ```
[**Next**: 🎹 Input](input.md)\ [**Previous**: ❌ Errors](errors.md)\ [**Top**: Table of contents](../readme.md#documentation)