execa vs cross-spawn vs spawn-sync
Process Management in Node.js Comparison
1 Year
execacross-spawnspawn-syncSimilar Packages:
What's Process Management in Node.js?

Process management libraries in Node.js provide tools for spawning and managing child processes from within a Node.js application. These libraries allow developers to execute external commands, scripts, or binaries, handle their input/output streams, and manage their execution in a non-blocking or synchronous manner. This is particularly useful for tasks like running shell commands, automating scripts, or integrating with other system-level processes. Each library offers different features and APIs to cater to various use cases, such as handling complex command execution, managing process lifecycles, and providing better error handling and performance.

Package Weekly Downloads Trend
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
execa88,044,0187,023324 kB122 months agoMIT
cross-spawn87,166,1231,12316.1 kB223 months agoMIT
spawn-sync866,34935-07 years agoMIT
Feature Comparison: execa vs cross-spawn vs spawn-sync

Cross-Platform Compatibility

  • execa:

    execa also provides good cross-platform compatibility, but it is built on top of the native child_process module, which means it inherits some of its quirks. However, execa offers a more modern API and better handles edge cases, making it a reliable choice for cross-platform applications.

  • cross-spawn:

    cross-spawn is designed to handle cross-platform compatibility issues when spawning child processes. It correctly handles the execution of commands on Windows, macOS, and Linux, ensuring that arguments are passed correctly regardless of the operating system.

  • spawn-sync:

    spawn-sync is a synchronous wrapper around the native child_process module. While it is cross-platform in nature, it does not provide any additional features or handling for cross-platform issues. It is best used in scenarios where platform-specific behavior is not a concern.

API Design

  • execa:

    execa offers a more modern and feature-rich API compared to the built-in child_process module. It supports promises and async/await, making it more suitable for modern JavaScript applications. The API is designed to be intuitive and easy to use, with additional features like execa.sync for synchronous execution.

  • cross-spawn:

    cross-spawn provides a simple and straightforward API for spawning child processes. It focuses on simplicity and ease of use, making it easy to integrate into existing projects without a steep learning curve.

  • spawn-sync:

    spawn-sync provides a minimalistic API for synchronously spawning child processes. It is straightforward and easy to use, but it lacks the advanced features and flexibility offered by the other libraries.

Error Handling

  • execa:

    execa offers much better error handling compared to the other libraries. It throws an ExecaError for failed processes, which includes detailed information about the error, such as the exit code, stdout, and stderr. This makes it easier to handle errors and debug issues when working with child processes.

  • cross-spawn:

    cross-spawn provides basic error handling for child processes. It emits errors when the process fails to spawn or exits with a non-zero status code. However, it does not provide detailed error information or support for promise-based error handling.

  • spawn-sync:

    spawn-sync provides basic error handling for synchronous process spawning. It throws an error if the process fails to spawn or exits with a non-zero status code, but it does not provide any additional error information or context.

Synchronous vs Asynchronous Execution

  • execa:

    execa supports both asynchronous and synchronous execution of child processes. It provides a promise-based API for async execution and also offers a synchronous method (execa.sync) for cases where blocking the main thread is acceptable.

  • cross-spawn:

    cross-spawn is primarily designed for asynchronous process spawning. It uses callbacks to handle process output and errors, allowing the main thread to continue executing while the child process runs in the background.

  • spawn-sync:

    spawn-sync is focused solely on synchronous execution. It blocks the main thread until the child process completes, making it suitable for scripts or applications where synchronous behavior is required.

Ease of Use: Code Examples

  • execa:

    Asynchronous process execution with execa

    const { execa } = require('execa');
    
    (async () => {
      try {
        const { stdout } = await execa('echo', ['Hello from Execa!']);
        console.log(stdout);
      } catch (error) {
        console.error(`Error: ${error.message}`);
      }
    })();
    
  • cross-spawn:

    Simple cross-platform process spawning with cross-spawn

    const spawn = require('cross-spawn');
    const child = spawn('echo', ['Hello, World!']);
    
    child.stdout.on('data', (data) => {
      console.log(`Output: ${data}`);
    });
    
    child.stderr.on('data', (data) => {
      console.error(`Error: ${data}`);
    });
    
    child.on('close', (code) => {
      console.log(`Child process exited with code ${code}`);
    });
    
  • spawn-sync:

    Synchronous process execution with spawn-sync

    const spawnSync = require('spawn-sync');
    const result = spawnSync('echo', ['Hello from Spawn-Sync!']);
    
    console.log(result.stdout.toString());
    if (result.error) {
      console.error(`Error: ${result.error.message}`);
    }
    
How to Choose: execa vs cross-spawn vs spawn-sync
  • execa:

    Choose execa if you need a feature-rich and modern alternative to the built-in child_process module. It offers a promise-based API, better error handling, and additional features like streaming, timeout support, and improved handling of subprocesses, making it ideal for more complex scenarios.

  • cross-spawn:

    Choose cross-spawn if you need a simple and reliable way to spawn child processes across different platforms, especially when dealing with cross-platform compatibility issues. It handles arguments and environment variables correctly, making it a good choice for most use cases.

  • spawn-sync:

    Choose spawn-sync if you require a synchronous method for spawning child processes. This is useful for scripts or applications where blocking the main thread is acceptable, and you need to ensure that the child process completes before continuing execution.

README for execa
execa logo

Coverage Status

Process execution for humans





Execa runs commands in your script, application or library. Unlike shells, it is optimized for programmatic usage. Built on top of the child_process core module.


One of the maintainers @ehmicky is looking for a remote full-time position. Specialized in Node.js back-ends and CLIs, he led Netlify Build, Plugins and Configuration for 2.5 years. Feel free to contact him on his website or on LinkedIn!


Features

Install

npm install execa

Documentation

Execution:

Input/output:

Advanced usage:

Examples

Execution

Simple syntax

import {execa} from 'execa';

const {stdout} = await execa`npm run build`;
// Print command's output
console.log(stdout);

Script

import {$} from 'execa';

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

const branch = await $`git branch --show-current`;
await $`dep deploy --branch=${branch}`;

await Promise.all([
	$`sleep 1`,
	$`sleep 2`,
	$`sleep 3`,
]);

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

Local binaries

$ npm install -D eslint
await execa({preferLocal: true})`eslint`;

Pipe multiple subprocesses

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

// Output of `npm run build | sort | head -n 2`
console.log(stdout);
// Output of `npm run build | sort`
console.log(pipedFrom[0].stdout);
// Output of `npm run build`
console.log(pipedFrom[0].pipedFrom[0].stdout);

Input/output

Interleaved output

const {all} = await execa({all: true})`npm run build`;
// stdout + stderr, interleaved
console.log(all);

Programmatic + terminal output

const {stdout} = await execa({stdout: ['pipe', 'inherit']})`npm run build`;
// stdout is also printed to the terminal
console.log(stdout);

Simple input

const getInputString = () => { /* ... */ };
const {stdout} = await execa({input: getInputString()})`sort`;
console.log(stdout);

File input

// Similar to: npm run build < input.txt
await execa({stdin: {file: 'input.txt'}})`npm run build`;

File output

// Similar to: npm run build > output.txt
await execa({stdout: {file: 'output.txt'}})`npm run build`;

Split into text lines

const {stdout} = await execa({lines: true})`npm run build`;
// Print first 10 lines
console.log(stdout.slice(0, 10).join('\n'));

Streaming

Iterate over text lines

for await (const line of execa`npm run build`) {
	if (line.includes('WARN')) {
		console.warn(line);
	}
}

Transform/filter output

let count = 0;

// Filter out secret lines, then prepend the line number
const transform = function * (line) {
	if (!line.includes('secret')) {
		yield `[${count++}] ${line}`;
	}
};

await execa({stdout: transform})`npm run build`;

Web streams

const response = await fetch('https://example.com');
await execa({stdin: response.body})`sort`;

Convert to Duplex stream

import {execa} from 'execa';
import {pipeline} from 'node:stream/promises';
import {createReadStream, createWriteStream} from 'node:fs';

await pipeline(
	createReadStream('./input.txt'),
	execa`node ./transform.js`.duplex(),
	createWriteStream('./output.txt'),
);

IPC

Exchange messages

// parent.js
import {execaNode} from 'execa';

const subprocess = execaNode`child.js`;
await subprocess.sendMessage('Hello from parent');
const message = await subprocess.getOneMessage();
console.log(message); // 'Hello from child'
// child.js
import {getOneMessage, sendMessage} from 'execa';

const message = await getOneMessage(); // 'Hello from parent'
const newMessage = message.replace('parent', 'child'); // 'Hello from child'
await sendMessage(newMessage);

Any input type

// main.js
import {execaNode} from 'execa';

const ipcInput = [
	{task: 'lint', ignore: /test\.js/},
	{task: 'copy', files: new Set(['main.js', 'index.js']),
}];
await execaNode({ipcInput})`build.js`;
// build.js
import {getOneMessage} from 'execa';

const ipcInput = await getOneMessage();

Any output type

// main.js
import {execaNode} from 'execa';

const {ipcOutput} = await execaNode`build.js`;
console.log(ipcOutput[0]); // {kind: 'start', timestamp: date}
console.log(ipcOutput[1]); // {kind: 'stop', timestamp: date}
// build.js
import {sendMessage} from 'execa';

const runBuild = () => { /* ... */ };

await sendMessage({kind: 'start', timestamp: new Date()});
await runBuild();
await sendMessage({kind: 'stop', timestamp: new Date()});

Graceful termination

// main.js
import {execaNode} from 'execa';

const controller = new AbortController();
setTimeout(() => {
	controller.abort();
}, 5000);

await execaNode({
	cancelSignal: controller.signal,
	gracefulCancel: true,
})`build.js`;
// build.js
import {getCancelSignal} from 'execa';

const cancelSignal = await getCancelSignal();
const url = 'https://example.com/build/info';
const response = await fetch(url, {signal: cancelSignal});

Debugging

Detailed error

import {execa, ExecaError} from 'execa';

try {
	await execa`unknown command`;
} catch (error) {
	if (error instanceof ExecaError) {
		console.log(error);
	}
	/*
	ExecaError: Command failed with ENOENT: unknown command
	spawn unknown ENOENT
			at ...
			at ... {
		shortMessage: 'Command failed with ENOENT: unknown command\nspawn unknown ENOENT',
		originalMessage: 'spawn unknown ENOENT',
		command: 'unknown command',
		escapedCommand: 'unknown command',
		cwd: '/path/to/cwd',
		durationMs: 28.217566,
		failed: true,
		timedOut: false,
		isCanceled: false,
		isTerminated: false,
		isMaxBuffer: false,
		code: 'ENOENT',
		stdout: '',
		stderr: '',
		stdio: [undefined, '', ''],
		pipedFrom: []
		[cause]: Error: spawn unknown ENOENT
				at ...
				at ... {
			errno: -2,
			code: 'ENOENT',
			syscall: 'spawn unknown',
			path: 'unknown',
			spawnargs: [ 'command' ]
		}
	}
	*/
}

Verbose mode

await execa`npm run build`;
await execa`npm run test`;
execa verbose output

Custom logging

import {execa as execa_} from 'execa';
import {createLogger, transports} from 'winston';

// Log to a file using Winston
const transport = new transports.File({filename: 'logs.txt'});
const logger = createLogger({transports: [transport]});
const LOG_LEVELS = {
	command: 'info',
	output: 'verbose',
	ipc: 'verbose',
	error: 'error',
	duration: 'info',
};

const execa = execa_({
	verbose(verboseLine, {message, ...verboseObject}) {
		const level = LOG_LEVELS[verboseObject.type];
		logger[level](message, verboseObject);
	},
});

await execa`npm run build`;
await execa`npm run test`;

Related

Maintainers