# Execa: running subprocesses from Node.js without a shell

> Execa wraps Node's child_process module with promises, template-string commands and structured errors. It is aimed at scripts, CLIs and libraries that need to call external binaries, and it trades shell convenience for predictable argument handling.

**sindresorhus/execa** — Process execution for humans

- Repository: https://github.com/sindresorhus/execa
- Stars: 7,610 · Forks: 1,570
- Language: JavaScript
- License: MIT
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/sindresorhus-execa

## The problem Execa solves for Node.js scripts

Node's built-in child_process module is enough to start a process, but it hands you streams, callbacks and exit codes that you then have to assemble yourself. Execa wraps that core module and presents the result as a promise, so a command becomes an awaitable value with stdout and stderr attached. The README describes it as process execution for humans and states that it is built on top of child_process.

The audience is specific. It is for people writing build scripts, CLIs, test harnesses and libraries in JavaScript or TypeScript that need to call git, npm, eslint, ffmpeg or any other binary. It is not a shell replacement for interactive use, and it is not a general job runner. The package is published on npm as execa, is written in JavaScript, is licensed MIT, and its package.json declares engines.node as >=22, which means the current major line targets modern Node only.

## How Execa avoids the shell and what that changes

The central design decision is that Execa does not go through a shell by default. The README states there is no escaping and no quoting needed, and no risk of shell injection. Instead of building a command string and handing it to /bin/sh, you pass the binary and its arguments, and Execa spawns the process directly.

That is why the template-string syntax works the way it does. Arguments interpolated into a tagged template are passed as separate arguments rather than concatenated into a string, so a value containing spaces or semicolons stays a single argument. The example in the README creates a directory named with a space:

```js
const directoryName = 'foo bar';
await $`mkdir /tmp/${directoryName}`;
```

The trade-off is real. Shell features such as globbing, variable expansion and redirection are not available unless you ask for a shell, and the README points to docs/shell.md for that. In exchange you get predictable argument boundaries, which matters when any part of the command comes from user input or from a configuration file.

## Installing Execa and running a first command

Installation is a single npm command. The README gives it exactly:

```bash
npm install execa
```

The package is ESM only. Its package.json sets type to module and exposes index.js plus index.d.ts through the exports field, so a CommonJS require will not resolve it. The simplest first use is the named execa export with a tagged template. The README's basic example looks like this:

```js
import {execa} from 'execa';

const {stdout} = await execa`npm run build`;
console.log(stdout);
```

After that call resolves, stdout holds the captured output of the command. If the command exits non-zero, the promise rejects with a detailed error object rather than returning a result, which is the behaviour to plan for in any caller.

There is a second interface. Importing the dollar-sign export gives a script-style API where commands can be chained with pipe, awaited in parallel, and given interpolated arguments:

```js
import {$} from 'execa';

const {stdout: name} = await $`cat package.json`.pipe`grep name`;
console.log(name);
```

That example is taken from the README's Script section. The pipe chains two subprocesses and the final result carries the output of the last one.

## Piping, local binaries and output capture

Execa's pipe interface is where it diverges most from a plain spawn wrapper. The README shows a three-stage pipeline and notes that intermediate results are retrievable, not discarded:

```js
const {stdout, pipedFrom} = await execa`npm run build`
	.pipe`sort`
	.pipe`head -n 2`;
```

The README states that stdout is the output of the full pipeline, pipedFrom[0].stdout is the output of the first two stages, and pipedFrom[0].pipedFrom[0].stdout is the output of the first command alone. A shell pipeline gives you only the last stage's output unless you add tee calls; Execa keeps the chain addressable. The documentation also covers multiple sources into one destination and one source into multiple destinations, plus an unpipe operation.

Local binaries are handled without npx. The README's example installs eslint as a dev dependency and then runs it with a preferLocal option:

```js
await execa({preferLocal: true})`eslint`;
```

Output handling is configurable rather than fixed. Setting all to true merges stdout and stderr in the order they arrived, and passing an array such as ['pipe', 'inherit'] to stdout both captures the stream and forwards it to the terminal. That array form is the answer to the common request to show output while also returning it to the caller.

## Where Execa is the wrong choice

Execa is not a shell, and anyone who wants shell semantics should use a shell. Globs, subshells, here-documents, environment assignment prefixes and redirection operators do not exist in the default execution path. The README addresses this directly in docs/shell.md, and it also documents the differences from Bash and zx in docs/bash.md. If your script is essentially a shell script, porting it to Execa means rewriting each construct.

The Node version floor is a second constraint. The engines field in package.json requires Node 22 or later, so projects pinned to older LTS lines cannot take this major version without upgrading the runtime first. Execa v10 is also ESM only, which means a CommonJS codebase needs a dynamic import or a build step.

Finally, the package is a convenience layer, not a sandbox. It removes shell injection by not invoking a shell, but it does not restrict which binaries you can run or what those binaries do with their arguments. Passing untrusted input as an argument to a dangerous command is still dangerous.

## Execa compared with zx and with plain child_process

The README names zx as a reference point in two places, in the Features list and in docs/bash.md, which is titled Difference with Bash and zx. The distinction it draws is that shells and zx are optimized for interactive or script-style use, while Execa is optimized for programmatic usage. In practice that means Execa returns values to your code and lets you handle errors as exceptions, whereas zx is built around writing shell-like scripts in JavaScript.

Against plain child_process the difference is smaller in capability and larger in ergonomics. Both ultimately spawn a process. Execa adds promise wrapping, the tagged template argument handling, line splitting, transform functions, IPC helpers, Windows handling for shebangs and PATHEXT, and termination logic that the README says forces subprocesses to exit even when they intercept termination signals or when the current process ends abruptly. If you already have a small, working child_process wrapper and no Windows or termination requirements, the migration may not pay for itself.

## Maintenance, licence and upgrade cost

The repository is not archived and the last push was on 2026-07-31, the same day v10.0.1 was released. The release history shows v10.0.0 on 2026-07-17 and v9.6.1 on 2025-11-29, so the current major line is recent and the previous major received a patch late in 2025.

The licence is MIT, declared in package.json and present as a license file at the repository root. MIT permits commercial use and modification, but the package includes a funding field pointing to GitHub Sponsors, and the README carries sponsor placements. That is a funding arrangement, not a licence term, and it does not change what you may do with the code. If your organisation requires legal review of dependency licences, the MIT text is the document to review.

The upgrade cost is concentrated in the v10 line. The engines field requires Node 22, and the package is ESM only through its exports map. A project on Node 18 or 20 cannot move to v10 without a runtime upgrade, and a CommonJS project needs an import strategy. The dependency list is modest and mostly small single-purpose packages from the same author, which keeps the transitive surface shallow but also means those packages carry their own version floors.

## Conclusion

Adopt Execa if your Node.js code calls external binaries and you want promise-based results, structured errors and no shell quoting. Do not adopt it if you need a shell pipeline with shell syntax, since Execa deliberately avoids the shell and its own pipe interface replaces it. Before committing, check that your runtime satisfies the engines field (node >=22), read docs/bash.md for the stated differences from Bash and zx, and confirm how your code will handle the detailed error object on non-zero exits.

## FAQ

### Why do I get 'execa is not a function'?

Execa v10 is ESM only and exports named bindings, so a CommonJS require of the package will not give you a callable default. Use an import statement, or a dynamic import inside CommonJS, and take the execa or dollar-sign export by name.

### How does execa differ from child_process?

Execa is built on top of the child_process core module, according to the README. It adds promise-based results, template-string commands that need no escaping or quoting, structured errors, piping between subprocesses, line splitting and termination handling that plain child_process does not provide.

### What is a real alternative to execa?

The README points to zx and to Bash in docs/bash.md, describing Execa as optimized for programmatic usage rather than for shell scripting. zx is built around writing shell-like scripts in JavaScript, while Execa is built around returning values to your code.

## Sources

- [Issues](https://github.com/sindresorhus/execa/issues)
- [License: MIT](https://github.com/sindresorhus/execa/blob/main/LICENSE)
- [README](https://github.com/sindresorhus/execa/blob/main/README.md)
- [Releases](https://github.com/sindresorhus/execa/releases)
- [sindresorhus/execa on GitHub](https://github.com/sindresorhus/execa)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sindresorhus-execa
