Tiny process execution for humans — a better
child_process
Features
No dependencies. Small package size:
Despite the small size, this is packed with some essential features:
- Promise-based interface.
- Iterate over the output lines.
- Pipe multiple subprocesses and retrieve intermediate results.
- Execute locally installed binaries without
npx
. - Improved Windows support.
- Proper handling of subprocess failures and better error messages.
- Get interleaved output from stdout and stderr similar to what is printed on the terminal.
- Strip unnecessary newlines.
- Pass strings as
stdin
input to the subprocess. - Preserve the current Node.js version and flags.
- Simpler syntax to set environment variables or
stdin
/stdout
/stderr
. - Compute the command duration.
For additional features, please check out Execa.
Install
npm install nano-spawn
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!
Usage
Run commands
import spawn from 'nano-spawn';
const result = await spawn('echo', ['🦄']);
console.log(result.output);
//=> '🦄'
Iterate over output lines
for await (const line of spawn('ls', ['--oneline'])) {
console.log(line);
}
//=> index.d.ts
//=> index.js
//=> …
Pipe commands
const result = await spawn('npm', ['run', 'build'])
.pipe('sort')
.pipe('head', ['-n', '2']);
API
spawn(file, arguments?, options?) default export
file
: string
arguments
: string[]
options
: Options
Returns: Subprocess
Executes a command using file ...arguments
.
This has the same syntax as child_process.spawn()
.
If file
is 'node'
, the current Node.js version and flags are inherited.
Options
options.stdio, options.shell, options.timeout, options.signal, options.cwd, options.killSignal, options.serialization, options.detached, options.uid, options.gid, options.windowsVerbatimArguments, options.windowsHide, options.argv0
All child_process.spawn()
options can be passed to spawn()
.
options.env
Type: object
Default: {}
Override specific environment variables. Other environment variables are inherited from the current process (process.env
).
options.preferLocal
Type: boolean
Default: false
Allows executing binaries installed locally with npm
(or yarn
, etc.).
options.stdin, options.stdout, options.stderr
Type: string | number | Stream | {string: string}
Subprocess's standard input/output/error.
All values supported by node:child_process
are available. The most common ones are:
'pipe'
(default value): returns the output usingresult.stdout
,result.stderr
andresult.output
.'inherit'
: uses the current process's input/output. This is useful when running in a terminal.'ignore'
: discards the input/output.Stream
: redirects the input/output from/to a stream. For example,fs.createReadStream()
/fs.createWriteStream()
can be used, once the stream'sopen
event has been emitted.{string: '...'}
: passes a string as input tostdin
.
Subprocess
Subprocess started by spawn()
.
await subprocess
Returns: Result
Throws: SubprocessError
A subprocess is a promise that is either resolved with a successful result
object or rejected with a subprocessError
.
subprocess.stdout
Returns: AsyncIterable<string>
Throws: SubprocessError
Iterates over each stdout
line, as soon as it is available.
The iteration waits for the subprocess to end (even when using break
or return
). It throws if the subprocess fails. This means you do not need to call await subprocess
.
subprocess.stderr
Returns: AsyncIterable<string>
Throws: SubprocessError
Same as subprocess.stdout
but for stderr
instead.
subprocess[Symbol.asyncIterator]()
Returns: AsyncIterable<string>
Throws: SubprocessError
Same as subprocess.stdout
but for both stdout
and stderr
.
subprocess.pipe(file, arguments?, options?)
file
: string
arguments
: string[]
options
: Options
Returns: Subprocess
Similar to the |
symbol in shells. Pipe the subprocess'sstdout
to a second subprocess's stdin
.
This resolves with that second subprocess's result. If either subprocess is rejected, this is rejected with that subprocess's error instead.
This follows the same syntax as spawn(file, arguments?, options?)
. It can be done multiple times in a row.
await subprocess.nodeChildProcess
Type: ChildProcess
Underlying Node.js child process.
Among other things, this can be used to terminate the subprocess using .kill()
or exchange IPC message using .send()
.
Result
When the subprocess succeeds, its promise is resolved with an object with the following properties.
result.stdout
Type: string
The output of the subprocess on standard output.
If the output ends with a newline, that newline is automatically stripped.
This is an empty string if either:
- The
stdout
option is set to another value than'pipe'
(its default value). - The output is being iterated using
subprocess.stdout
orsubprocess[Symbol.asyncIterator]
.
result.stderr
Type: string
Like result.stdout
but for the standard error instead.
result.output
Type: string
Like result.stdout
but for both the standard output and standard error, interleaved.
result.command
Type: string
The file and arguments that were run.
It is intended for logging or debugging. Since the escaping is fairly basic, it should not be executed directly.
result.durationMs
Type: number
Duration of the subprocess, in milliseconds.
result.pipedFrom
Type: Result | SubprocessError | undefined
If subprocess.pipe()
was used, the result or error of the other subprocess that was piped into this subprocess.
SubprocessError
Type: Error
When the subprocess fails, its promise is rejected with this error.
Subprocesses fail either when their exit code is not 0
or when terminated by a signal. Other failure reasons include misspelling the command name or using the timeout
option.
Subprocess errors have the same shape as successful results, with the following additional properties.
This error class is exported, so you can use if (error instanceof SubprocessError) { ... }
.
subprocessError.exitCode
Type: number | undefined
The numeric exit code of the subprocess that was run.
This is undefined
when the subprocess could not be started, or when it was terminated by a signal.
subprocessError.signalName
Type: string | undefined
The name of the signal (like SIGTERM
) that terminated the subprocess, sent by either:
- The current process.
- Another process. This case is not supported on Windows.
If a signal terminated the subprocess, this property is defined and included in the error message. Otherwise it is undefined
.
Windows support
This package fixes several cross-platform issues with node:child_process
. It brings full Windows support for:
- Node modules binaries (without requiring the
shell
option). This includes runningnpm ...
oryarn ...
. .cmd
,.bat
, and other shell files.- The
PATHEXT
environment variable. - Windows-specific newlines.
Alternatives
nano-spawn
's main goal is to be small, yet useful. Nonetheless, depending on your use case, there are other ways to run subprocesses in Node.js.
Execa
Execa is a similar package: it provides the same features, but more. It is also built on top of node:child_process
, and is maintained by the same people.
On one hand, it has a bigger size:
On the other hand, it provides a bunch of additional features: scripts, template string syntax, synchronous execution, file input/output, binary input/output, advanced piping, verbose mode, graceful or forceful termination, IPC, shebangs on Windows, and much more. Also, it is very widely used and battle-tested.
We recommend using Execa in most cases, unless your environment requires using small packages (for example, in a library or in a serverless function). It is definitely the best option inside scripts, servers, or apps.
node:child_process
nano-spawn
is built on top of the node:child_process
core module.
If you'd prefer avoiding adding any dependency, you may use node:child_process
directly. However, you might miss the features nano-spawn
provides: proper error handling, full Windows support, local binaries, piping, lines iteration, interleaved output, and more.
import {execFile} from 'node:child_process';
import {promisify} from 'node:util';
const pExecFile = promisify(execFile);
const result = await pExecFile('npm', ['run', 'build']);