Simon Plenderleith Simon is an independent Node.js consultant and educator. He helps developers level up with Node.js at simonplend.com. He's on Twitter at @simonplend if you'd like to say hi.

Running commands with execa in Node.js

7 min read 2075

Execa Logo

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.

What is execa?

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:

  • Familiarity with the basics of JavaScript and Node.js
  • To be comfortable running commands in a terminal
  • To have Node.js >/= v14.13.1 installed
  • To be running Node.js on either macOS, Linux, or WSL on Windows (Windows Subsystem for Linux)

All the code in this article is available on GitHub.

The benefits of using execa

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.

We made a custom demo for .
No really. Click here to check it out.

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.

Getting started with execa

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

Using a pure ES module package in Node.js

Node.js supports two different module types:

  • CommonJS Modules (CJS): uses module.exports to export functions and objects and require() to load them in another module
  • ECMAScript Modules (ESM): uses export to export functions and objects and import to load them in another module

Execa 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.

Running a command with execa

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!' }

What is 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 to
  • stderr: standard error is the stream that a program writes error messages and debugs data to

When 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.

Working with execa

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.

Capturing 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.

Error handling in execa

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)

Canceling a child process

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.

Piping output from a child process with execa

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.

Redirecting output to a file

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?

Conclusion

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.

200’s only Monitor failed and slow network requests in production

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. https://logrocket.com/signup/

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. .
Simon Plenderleith Simon is an independent Node.js consultant and educator. He helps developers level up with Node.js at simonplend.com. He's on Twitter at @simonplend if you'd like to say hi.

Leave a Reply