Node.js has a built-in child_process
module. It provides methods that allow us to write scripts that run a command in a child process. These commands can run any programs that are installed on the machine that we’re running the script on.
The execa library provides a wrapper around the child_process
module for ease of use. This popular library was created and is maintained by prolific open-source developer Sindre Sorhus, and it’s downloaded millions of times every week.
In this article, we’re going to learn about the benefits of the execa
library and how we can start using it. We’ll also dive into the basics of error handling and process control, as well as look at the options we have for handling child process output.
To follow along, you’ll need:
All the code in this article is available on GitHub.
There are several benefits that execa provides over the built-in Node.js child_process
module.
First, execa exposes a promise-based API. This means we can use async/await in our code instead of needing to create callback functions as we do with the asynchronous child_process
module methods. If we need it, there is also an execaSync
method for running commands synchronously.
Execa also conveniently escapes and quotes any command arguments that we pass to it. For example, if we’re passing a string with spaces or quotes, execa will take care of escaping them for us.
It’s common for programs to include a new line at the end of their output. This is convenient for readability on the command line, but not helpful in a scripting context, so execa automatically strips these new lines for us by default.
We probably don’t want child processes hanging around if the process that is executing our script (node
) dies for any reason. Execa takes care of child process clean-up for us automatically, ensuring we don’t end up with “zombie” processes.
Another benefit of using execa is that it brings its support for running child processes with Node.js on Windows. It uses the popular cross-spawn package, which works around known issues with Node.js’ child_process.spawn
method on Windows.
Open up your terminal and create a new directory named tutorial
, then change into that directory:
mkdir tutorial cd tutorial
Now, let’s create a new package.json
file:
npm init --yes
Next, install the execa library:
npm install execa
Node.js supports two different module types:
module.exports
to export functions and objects and require()
to load them in another moduleexport
to export functions and objects and import
to load them in another moduleExeca becomes a pure ESM package with its v6.0.0 release. This means we must use a version of Node.js that has support for ES modules in order to use this package.
For our own code, we can show Node.js that all modules in our project are ES modules by adding "type": "module"
in our package.json
, or we can set the file extension of individual scripts to .mjs
.
Let’s update our package.json
file:
{ "name": "tutorial", + "type": "module",
We can now import the execa method from the execa
package in any scripts we create:
import { execa } from "execa";
Although ES modules are seeing adoption in Node.js packages and applications, CommonJS modules are still the most widely used module type. If you’re unable to use ES modules in your codebase for any reason, you’ll need to use the import
method in your CommonJS modules:
async function run() { const { execa } = await import("execa"); // Code which uses `execa` here. } run();
Note that we need to wrap the call to the import
function in an async
function, as CommonJS modules do not support top-level await.
Now we’re going to create a script that uses execa to run the following command:
echo "execa is pretty neat!"
The echo
program prints out the string of text that is passed to it.
Let’s create a new file named run.js
. In this script, we’ll import the execa
method from the execa package:
// run.js import { execa } from "execa";
Then, we’ll run the echo
command with the execa
method:
// run.js const { stdout } = await execa("echo", ["execa is pretty neat!"]);
The promise returned by execa
resolves with an object. You’ll notice in the code above that the stdout
property is being unpacked from this object and assigned to a variable.
Let’s log the stdout
variable so we can see what its value is:
// run.js console.log({ stdout });
Now we can run the script:
node run.js
We should see the following output:
{ stdout: 'execa is pretty neat!' }
stdout
?Programs have access to “standard streams”. In this tutorial, we’ll work with two of them:
stdout
: standard output is the stream that a program writes its output data tostderr
: standard error is the stream that a program writes error messages and debugs data toWhen we run a program, these standard streams are typically connected to the parent process. If we’re running a program in our terminal, this means data sent by the program to the stdout
and stderr
streams will be received and displayed by the terminal.
When we run the scripts in this tutorial, the stdout
and stderr
streams of the child process are connected to the parent process, node
, allowing us to access any data the child process sends to them.
Now that we have a script that can run a command in a child process, we’re going to dive into the basics of working with execa.
stderr
Let’s change the line in run.js
where the execa
method is called to unpack the stderr
property from the object:
- const { stdout } = await execa("echo", ["execa is pretty neat!"]); + const { stdout, stderr } = await execa("echo", ["execa is pretty neat!"]);
Then update the console.log
line:
- console.log({ stdout }); + console.log({ stdout, stderr });
Now, let’s run the script again:
node run.js
We should see the following output:
{ stdout: 'execa is pretty neat!', stderr: '' }
You’ll notice that the value of stderr
is an empty string (''
). This is because the echo
command has written no data to the standard error stream.
The ls(1) program lists information about files and directories. If a file does not exist, it will write an error message to the standard error stream.
Let’s replace the command which we’re executing in run.js
:
- const { stdout, stderr } = await execa("echo", ["execa is pretty neat!"]); + const { stdout, stderr } = await execa("ls", ["missing-file.txt"]);
When we run the script (node run.js
), we should now see the following output:
Error: Command failed with exit code 2: ls missing-file.txt ... { shortMessage: 'Command failed with exit code 2: ls missing-file.txt', command: 'ls missing-file.txt', escapedCommand: 'ls missing-file.txt', exitCode: 2, signal: undefined, signalDescription: undefined, stdout: '', stderr: "ls: cannot access 'missing-file.txt': No such file or directory", failed: true, timedOut: false, isCanceled: false, killed: false }
Running this command with the execa
method has thrown an error. This is because the child process’ exit code was not 0
. An exit code of 0
commonly shows that a process ran successfully, and any other value indicates that there was a problem. The manual page for ls
defines the following exit codes and their meaning:
Exit status: 0 if OK, 1 if minor problems (e.g., cannot access subdirectory), 2 if serious trouble (e.g., cannot access command-line argument).
The error object thrown by the execa
method contains a stderr
property with the data that was written by ls
to the standard error stream.
We now need to implement error handling so that if the command fails, we handle it gracefully, rather than letting it crash our script.
Note: programs will often run successfully, but also write debug messages to stderr
.
We can wrap the command in a try...catch
statement and output a custom error message, like this:
// run.js try { const { stdout, stderr } = await execa("ls", ["missing-file.txt"]); console.log({ stdout, stderr }); } catch (error) { console.error( `ERROR: The command failed. stderr: ${error.stderr} (${error.exitCode})` ); }
Now, when we run our script (node run.js
), we should see the following output:
ERROR: The command failed. stderr: ls: cannot access 'missing-file.txt': No such file or directory (2)
Once we’ve started executing a command, we might want to cancel the process, e.g., if it takes longer than expected to complete. Execa provides a cancel
method that we can call to send a SIGTERM
signal to the child process.
Let’s replace all the code in run.js
apart from the import
statement. We’ll use the sleep program to create a child process that runs for five seconds:
// run.js const subprocess = execa("sleep", ["5s"]);
You’ll notice there is no await
keyword in front of this function call. This is because we want to first define a function that will run after one second and cancel the child process:
// run.js setTimeout(() => { subprocess.cancel(); }, 1000);
Then we can await
the subprocess
promise inside a try...catch
block:
// run.js try { const { stdout, stderr } = await subprocess; console.log({ stdout, stderr }); } catch (error) { if (error.isCanceled) { console.error(`ERROR: The command took too long to run.`) } else { console.error(error); } }
When the subprocess.cancel
method has been called, the isCanceled
property on the error object is set to true
. This allows us to determine the cause of the error and display a specific error message.
Now, when we run our script (node run.js
), we should see the following output:
ERROR: The command took too long to run.
If we need to force a child process to end, we can call the subprocess.kill
method instead of subprocess.cancel
.
The stdout
and stderr
properties in the object returned by the execa
method are readable streams. We’ve already seen how we can save the data that is sent to these streams in variables. We can pipe readable streams into writeable streams, for example, to see the output of the child process as it runs.
Let’s remove the call to setTimeout
in run.js
:
- setTimeout(() => { - subprocess.cancel(); - }, 1000);
Let’s change our command to use echo
again:
- const subprocess = execa("sleep", ["5s"]); + const subprocess = execa("echo", ["is it me you're looking for?"]);
Then, we’ll pipe the stdout
stream from the child process into the stdout
stream of our parent process (node
):
subprocess.stdout.pipe(process.stdout);
Now, when we run the script, we should see the following output:
is it me you're looking for? { stdout: "is it me you're looking for?", stderr: '' }
Our script now outputs the data from the child process’ stderr
stream as it runs, then logs out the values of the stderr
and stdout
variables to which we’ve saved the streams’ data.
Instead of piping the child process’ stdout
data to the stdout
stream of our parent process, we could write it to a file instead.
First, we need to import
the built-in Node.js fs module:
// run.js import * as fs from "fs";
Then we can replace the existing call to the pipe
method:
- subprocess.stdout.pipe(process.stdout); + subprocess.stdout.pipe(fs.createWriteStream("stdout.txt"));
This creates an fs.WriteStream
instance where data from the subprocess.stdout
readable stream will be piped to.
When we run our script, we should see the following output:
{ stdout: "is it me you're looking for?", stderr: '' }
We should also see that a file named stdout.txt
has been created, containing the data from the child process’ stdout
stream:
$ cat stdout.txt is it me you're looking for?
In this article, we’ve learned how to use execa to run commands from our Node.js scripts. We’ve seen the benefits it provides over the built-in Node.js child_process
module and learned the basic patterns that we need to apply to use execa effectively.
All the code in this article is available on GitHub.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]