child_process vs execa vs node-cmd vs shelljs
Executing System Commands in Node.js Environments
child_processexecanode-cmdshelljsSimilar Packages:

Executing System Commands in Node.js Environments

These tools enable Node.js applications to run system commands and interact with the operating shell. child_process is the built-in core module providing low-level access. execa is a modern promise-based wrapper designed for better developer experience. node-cmd offers a simplified callback interface for running commands. shelljs focuses on portable Unix shell commands across platforms, allowing scripts to run without relying on specific OS shells.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
child_process0165-410 years agoISC
execa07,458325 kB163 months agoMIT
node-cmd0285-75 years agoMIT
shelljs014,417152 kB10110 months agoBSD-3-Clause

Running System Commands: child_process vs execa vs node-cmd vs shelljs

When building Node.js tools, build scripts, or automation workflows, you often need to run system commands. The ecosystem offers several ways to do this, ranging from the built-in core module to high-level wrappers. Let's compare how child_process, execa, node-cmd, and shelljs handle common tasks.

πŸš€ Basic Command Execution

child_process is the foundation. It requires importing the module and using methods like exec or spawn. It relies on callbacks or events.

const { exec } = require('child_process');

exec('ls -la', (error, stdout, stderr) => {
  if (error) {
    console.error(`Error: ${error}`);
    return;
  }
  console.log(`Output: ${stdout}`);
});

execa simplifies this with promises and async/await. It returns the output directly without needing a callback.

const execa = require('execa');

(async () => {
  const { stdout } = await execa('ls', ['-la']);
  console.log(`Output: ${stdout}`);
})();

node-cmd wraps child_process to reduce boilerplate but still uses callbacks by default.

const cmd = require('node-cmd');

cmd.get('ls -la', (error, data, stderr) => {
  if (error) {
    console.error(`Error: ${error}`);
    return;
  }
  console.log(`Output: ${data}`);
});

shelljs provides Unix-like commands. For arbitrary commands, it uses exec, but it shines with built-in methods.

const shell = require('shelljs');

const result = shell.exec('ls -la');
if (result.code !== 0) {
  console.error(`Error: ${result.stderr}`);
} else {
  console.log(`Output: ${result.stdout}`);
}

⚠️ Handling Errors

Error handling varies significantly between these tools. Proper error management prevents silent failures in automation.

child_process passes the error as the first argument in the callback. You must check it manually.

const { exec } = require('child_process');

exec('nonexistent-command', (error, stdout, stderr) => {
  if (error) {
    // Error object contains code, signal, etc.
    console.error(`Failed: ${error.message}`);
  }
});

execa throws an error that includes stdout and stderr properties. This makes debugging much easier in try/catch blocks.

const execa = require('execa');

try {
  await execa('nonexistent-command');
} catch (error) {
  // Contains stdout, stderr, and original message
  console.error(`Failed: ${error.message}`);
  console.error(`Output: ${error.stdout}`);
}

node-cmd follows the standard Node.js callback error pattern. It does not enhance the error object with output data by default.

const cmd = require('node-cmd');

cmd.get('nonexistent-command', (error, data, stderr) => {
  if (error) {
    // Must rely on stderr string for details
    console.error(`Failed: ${stderr}`);
  }
});

shelljs returns an object with a code property. You check the code to determine success or failure.

const shell = require('shelljs');

const result = shell.exec('nonexistent-command');
if (result.code !== 0) {
  // Check exit code and stderr
  console.error(`Failed with code ${result.code}`);
}

πŸ“‘ Streaming Output

For long-running tasks, streaming output is better than waiting for completion. This keeps the user informed.

child_process uses spawn for streaming. You listen to data events on stdout and stderr streams.

const { spawn } = require('child_process');

const child = spawn('ls', ['-la']);
child.stdout.on('data', (data) => {
  console.log(`Output: ${data}`);
});
child.stderr.on('data', (data) => {
  console.error(`Error: ${data}`);
});

execa supports streaming via the stdout and stderr properties which are streams. It also offers execa.command for convenience.

const execa = require('execa');

const child = execa('ls', ['-la']);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

node-cmd does not support streaming natively. It buffers the output and returns it all at once in the callback. This can cause memory issues with large outputs.

const cmd = require('node-cmd');

// No streaming support
// Output is buffered entirely before callback fires
cmd.get('ls -la', (error, data) => {
  console.log(data);
});

shelljs can stream if you pipe the result, but its exec method buffers by default. You can enable silent mode to handle output manually.

const shell = require('shelljs');

// Buffering by default
const result = shell.exec('ls -la', { silent: false });
// To stream, you often need to mix with child_process

🌐 Cross-Platform Scripting

Running scripts on Windows, macOS, and Linux can be painful due to shell differences. Some tools help bridge this gap.

child_process runs commands using the default system shell. You must handle path separators and command differences manually.

const { exec } = require('child_process');

// Windows uses 'dir', Unix uses 'ls'
// You need logic to detect OS
const command = process.platform === 'win32' ? 'dir' : 'ls';
exec(command);

execa handles some shell differences but still relies on the underlying system commands. It offers a shell option to specify the binary.

const execa = require('execa');

// Explicitly set shell if needed
await execa('ls', ['-la'], { shell: true });

node-cmd behaves like child_process regarding platform differences. It does not abstract command names.

const cmd = require('node-cmd');

// Same manual OS checking required
const command = process.platform === 'win32' ? 'dir' : 'ls';
cmd.get(command);

shelljs is designed for portability. It implements commands like cp, rm, and mkdir in JavaScript, so they work the same everywhere.

const shell = require('shelljs');

// Works on Windows and Unix without changes
shell.cp('-R', 'source/', 'dest/');
shell.rm('-rf', 'temp/');

πŸ› οΈ Maintenance and Future Proofing

Choosing a library involves checking its long-term support. Unmaintained packages can introduce security risks or break with new Node.js versions.

child_process is part of Node.js core. It receives updates with every Node.js release. It is stable and guaranteed to exist.

execa is actively maintained with frequent updates. It adopts new JavaScript features quickly and has a large community. It is the modern standard for this task.

node-cmd has seen very little activity in recent years. It lacks modern features like promises and robust error handling. It should be considered legacy.

shelljs is maintained but focuses on specific use cases. It is stable for file operations but less optimal for general process management compared to execa.

πŸ“Š Summary Table

Featurechild_processexecanode-cmdshelljs
TypeCore Modulenpm Packagenpm Packagenpm Package
API StyleCallbacks / EventsPromises / AsyncCallbacksSync / Async Mix
Error DataManual CheckIncluded in ErrorManual CheckExit Code
StreamingYes (spawn)YesNoLimited
PortabilityLowMediumLowHigh (file ops)
StatusStableActiveLegacyActive

πŸ’‘ Final Recommendation

execa is the best choice for most developers today. It reduces boilerplate, handles errors gracefully, and supports modern async patterns. Use it for CLI tools, build scripts, and automation.

child_process remains essential for low-level control or when you cannot add dependencies. It is best for core infrastructure where every kilobyte counts.

shelljs is ideal for write-once-run-anywhere scripts involving file operations. Use it when you want Unix command familiarity without shell compatibility issues.

node-cmd should be avoided in new projects. It offers no significant advantage over execa and lacks active maintenance. Migrate existing usage to execa for better reliability.

Final Thought: While child_process gives you the engine, execa gives you the steering wheel. Choose based on how much control you need versus how much speed you want in development.

How to Choose: child_process vs execa vs node-cmd vs shelljs

  • child_process:

    Choose child_process when you need maximum control over process execution without adding external dependencies. It is ideal for core infrastructure tools where minimizing bundle size is critical and you are comfortable managing streams and callbacks manually. Avoid it for simple scripts where modern async syntax would improve readability.

  • execa:

    Choose execa for most modern Node.js projects requiring command execution. It provides promise-based APIs, better error handling, and sensible defaults for stripping output. It is the best fit for CLI tools, build scripts, and automation tasks where reliability and clean code are priorities.

  • node-cmd:

    Choose node-cmd only for maintaining legacy scripts that already depend on it. It is not recommended for new projects because it lacks active maintenance and modern features like promise support. Evaluate execa instead for better long-term stability and error handling.

  • shelljs:

    Choose shelljs when you need to write portable shell scripts that run across Windows, macOS, and Linux without changing code. It is best for file operations like copy, move, and remove within scripts. Use it when you want Unix command familiarity without relying on the underlying system shell.

README for child_process

Security holding package

This package name is not currently in use, but was formerly occupied by another package. To avoid malicious use, npm is hanging on to the package name, but loosely, and we'll probably give it to you if you want it.

You may adopt this package by contacting support@npmjs.com and requesting the name.