cross-spawn, execa, and spawn-sync are utilities designed to handle child process execution in Node.js, addressing common pain points like cross-platform compatibility and synchronous execution. cross-spawn acts as a robust replacement for the native child_process.spawn, fixing Windows-specific issues with shebangs and PATH resolution. execa builds on top of cross-spawn to provide a higher-level, Promise-based API with improved error handling and streaming support. spawn-sync was historically used to polyfill synchronous spawning for older Node versions, but its utility has diminished as modern Node.js includes spawnSync natively.
Running system commands is a common requirement in Node.js development, whether you are building CLI tools, automating deployment scripts, or running build processes. The native child_process module provides the foundation, but it has historical quirks β especially on Windows. cross-spawn, execa, and spawn-sync emerged to solve these problems, but they serve different roles in the ecosystem.
The fundamental difference lies in how these packages handle process execution and result retrieval.
cross-spawn is a direct replacement for child_process.spawn. It returns a ChildProcess instance, meaning you handle data via streams and events.
// cross-spawn: Event-based streaming
const spawn = require('cross-spawn');
const child = spawn('npm', ['install', 'lodash']);
child.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
child.on('close', (code) => {
console.log(`Child process exited with code ${code}`);
});
execa wraps cross-spawn and returns a Promise. It buffers output by default, making async/await patterns clean and simple.
// execa: Promise-based with buffered output
const { execa } = require('execa');
async function installPackage() {
try {
const { stdout } = await execa('npm', ['install', 'lodash']);
console.log(stdout);
} catch (error) {
console.error(error.message);
}
}
spawn-sync forces synchronous execution, blocking the event loop until the command finishes. It was designed to bring spawnSync to older Node versions.
// spawn-sync: Blocking execution (Legacy)
const spawnSync = require('spawn-sync');
const result = spawnSync('npm', ['install', 'lodash']);
if (result.status === 0) {
console.log(result.output.toString());
} else {
console.error('Command failed');
}
One of the primary reasons to use cross-spawn or execa is to avoid Windows-specific failures. The native child_process.spawn often fails on Windows when trying to execute scripts with shebangs (like #!/usr/bin/env node) or when commands are not .exe files.
cross-spawn explicitly handles PATHEXT and shebang parsing. It ensures that running eslint works the same way on Windows as it does on macOS.
// cross-spawn: Handles shebangs automatically
const spawn = require('cross-spawn');
// This works on Windows even if 'eslint' is a shell script
const child = spawn('eslint', ['--fix', 'src/']);
execa inherits this behavior since it uses cross-spawn internally. You get the same cross-platform reliability with less code.
// execa: Inherits cross-spawn compatibility
const { execa } = require('execa');
// No extra configuration needed for Windows compatibility
await execa('eslint', ['--fix', 'src/']);
spawn-sync also attempted to solve this for synchronous calls on older Node versions. However, since Node v4.0.0, the native child_process.spawnSync has included these fixes.
// spawn-sync: Legacy polyfill behavior
const spawnSync = require('spawn-sync');
// Historically needed for Node < 0.12, now obsolete
const result = spawnSync('eslint', ['--fix', 'src/']);
How each package reports failures significantly impacts debugging and stability.
cross-spawn emits an error event if the process fails to spawn (e.g., command not found). Exit codes must be checked manually in the close event.
// cross-spawn: Manual error checking
child.on('error', (err) => {
console.error('Failed to start subprocess:', err);
});
child.on('close', (code) => {
if (code !== 0) {
console.error(`Process exited with code ${code}`);
}
});
execa throws an error if the exit code is non-zero. It includes the stdout, stderr, and command arguments in the error object, which is invaluable for debugging.
// execa: Automatic error throwing with context
try {
await execa('unknown-command');
} catch (error) {
// error.stdout, error.stderr, and error.command are available
console.error(`Command failed: ${error.message}`);
}
spawn-sync returns an object with a status property. It does not throw by default; you must check the status code manually.
// spawn-sync: Check status property
const result = spawnSync('unknown-command');
if (result.status !== 0) {
console.error('Command failed with status', result.status);
}
It is critical to note the maintenance status of these packages in the context of modern Node.js.
cross-spawn remains actively maintained and is the standard solution for cross-platform spawning. It is safe for production use.
execa is also actively maintained and widely adopted in the ecosystem. It is the recommended choice for new projects requiring process execution.
spawn-sync is effectively deprecated by the evolution of Node.js itself. The feature it provided (synchronous spawning) is now built into the core child_process module. Using it adds unnecessary dependencies to your project.
// Modern Native Alternative to spawn-sync
const { spawnSync } = require('child_process');
// Use native API instead of external package
const result = spawnSync('npm', ['--version']);
console.log(result.stdout.toString());
| Feature | cross-spawn | execa | spawn-sync |
|---|---|---|---|
| Execution Style | Event Stream (Async) | Promise (Async) | Blocking (Sync) |
| Windows Support | β Full (Shebang/PATH) | β Full (Inherited) | β Legacy Polyfill |
| Error Handling | Manual (Events) | Automatic (Throw) | Manual (Status Check) |
| Node Version | All | Modern (v14+) | Legacy (< v0.12) |
| Maintenance | β Active | β Active | β οΈ Obsolete |
cross-spawn is the engine under the hood. Use it if you are building a library that needs to spawn processes with maximum control and minimal abstraction. It is the safe bet for ensuring your tool works on Windows without extra configuration.
execa is the complete vehicle. For most application developers, this is the right choice. It saves time on error handling, input/output management, and promise wrapping. It turns a complex native API into a simple function call.
spawn-sync is a museum piece. Do not use it in new projects. If you need synchronous execution, use the native child_process.spawnSync or execa.sync. Relying on external polyfills for core Node.js features introduces risk without benefit.
Final Thought: In modern Node.js development, start with execa for its developer experience. Drop down to cross-spawn only if you need specific stream control that execa abstracts away. Leave spawn-sync in the past.
Choose cross-spawn when you need a reliable, low-level replacement for child_process.spawn that works consistently across Windows, macOS, and Linux. It is ideal if you want to fix cross-platform path issues without adopting a higher-level abstraction or Promise-based workflow. This package is best for tools that require fine-grained control over the spawned process streams and events.
Choose execa for most modern Node.js projects where you need to run commands asynchronously with clean, Promise-based code. It is the best choice for CLI tools, build scripts, or backend services that require timeout handling, input/output streaming, and human-friendly error messages out of the box. It simplifies complex process management tasks that would otherwise require significant boilerplate with native APIs.
Avoid spawn-sync in new projects unless you are maintaining legacy codebases that must run on Node.js versions older than v0.12. Modern Node.js environments include child_process.spawnSync natively, making this package obsolete. If you need synchronous execution today, rely on the built-in Node API or use execa.sync for a more ergonomic experience.
A cross platform solution to node's spawn and spawnSync.
Node.js version 8 and up:
$ npm install cross-spawn
Node.js version 7 and under:
$ npm install cross-spawn@6
Node has issues when using spawn on Windows:
./my-folder/my-executable)node_modules/.bin/), where arguments with quotes and parenthesis would result in invalid syntax erroroptions.shell support on node <v4.8All these issues are handled correctly by cross-spawn.
There are some known modules, such as win-spawn, that try to solve this but they are either broken or provide faulty escaping of shell arguments.
Exactly the same way as node's spawn or spawnSync, so it's a drop in replacement.
const spawn = require('cross-spawn');
// Spawn NPM asynchronously
const child = spawn('npm', ['list', '-g', '-depth', '0'], { stdio: 'inherit' });
// Spawn NPM synchronously
const result = spawn.sync('npm', ['list', '-g', '-depth', '0'], { stdio: 'inherit' });
options.shell as an alternative to cross-spawnStarting from node v4.8, spawn has a shell option that allows you run commands from within a shell. This new option solves
the PATHEXT issue but:
<v4.8If you are using the shell option to spawn a command in a cross platform way, consider using cross-spawn instead. You have been warned.
options.shell supportWhile cross-spawn adds support for options.shell in node <v4.8, all of its enhancements are disabled.
This mimics the Node.js behavior. More specifically, the command and its arguments will not be automatically escaped nor shebang support will be offered. This is by design because if you are using options.shell you are probably targeting a specific platform anyway and you don't want things to get into your way.
While cross-spawn handles shebangs on Windows, its support is limited. More specifically, it just supports #!/usr/bin/env <program> where <program> must not contain any arguments.
If you would like to have the shebang support improved, feel free to contribute via a pull-request.
Remember to always test your code on Windows!
$ npm test
$ npm test -- --watch during development
Released under the MIT License.