spawndamnit
spawndamnit is a very small CommonJS wrapper around `child_process.spawn()`. It uses `cross-spawn` for command lookup, returns a Promise that resolves when the child closes, buffers stdout and stderr, emits each output chunk as it arrives, and sends SIGTERM to children still active when the parent exits. Its entire public surface is essentially one function plus the unusual Promise and EventEmitter hybrid it returns.
A compact fit for old-style CommonJS tools that want exactly buffered output, live chunk events, and an awaitable close result. New code usually gets safer defaults, types, cancellation, and better failure handling from execa, or clearer control from built-in `spawn()`.
Use it if
- You maintain a CommonJS CLI that wants `spawn()` argument safety plus a simple awaitable result
- You need both live stdout and stderr events and final Buffer values without writing the collection code yourself
- You need Windows-friendly executable lookup through cross-spawn
- You accept that a nonzero exit code is a normal resolved result and will check it explicitly
- You need TypeScript declarations, ESM exports, AbortSignal cancellation, timeouts, input helpers, IPC, or a documented way to kill one running child; version 3.0.1 supplies none of those
- Child output can be large or unbounded; the source retains every stdout and stderr chunk in memory and repeatedly combines Buffers, with no maximum-output option
- You expect `await` to throw when a command exits nonzero; the implementation resolves `{ code, stdout, stderr }` for every normal close and rejects only spawn errors
- You need the actual ChildProcess object, its PID, signal result, or lifecycle events beyond output; the wrapper exposes a Promise/EventEmitter hybrid rather than the spawned child
- You want a well-documented, actively evolving process runner; the README has one example, the package has no tests or types in its published files, and the repository has had no push since the November 2024 release
Setup reality
`npm install spawndamnit` is enough and there are no peer dependencies or native builds. The runtime is CommonJS, so use `require()` in CommonJS or Node's default-import interop from ESM; there is no `exports` map and no TypeScript declaration file. The function accepts the same command, argument array, and options shape as `child_process.spawn()`, but the README does not document those options. Pass arguments as an array and leave `shell` off whenever input can be influenced by users. The returned object is both a Promise and an EventEmitter: attach `stdout` and `stderr` listeners before awaiting it, because fast commands may emit immediately. A normal nonzero exit does not reject, so every caller must inspect `code`. A command that cannot be spawned rejects. Output is always accumulated into memory when pipes exist, even if you only care about the exit code; there is no cap, streaming-only switch, timeout, or AbortSignal support. Setting `stdio: 'inherit'` means the child streams are absent and the result Buffers remain empty. The wrapper tracks active children globally and sends SIGTERM when the parent exits, but it does not expose each ChildProcess, so callers cannot directly read the PID or kill a single task through the documented API. If you need those controls, use Node's built-in process APIs or a fuller runner such as execa.
Patterns
Run a command and inspect its exit coderun-command
const spawn = require('spawndamnit');
const { code, stdout, stderr } = await spawn('git', ['status', '--short']);
if (code !== 0) {
throw new Error(stderr.toString('utf8') || `git exited ${code}`);
}
console.log(stdout.toString('utf8'));A nonzero exit code resolves rather than rejects, so treating failure as an exception is the caller's job.
Print output while retaining the final buffersstream-live-output
const child = spawn('npm', ['run', 'build']);
child.on('stdout', (chunk) => process.stdout.write(chunk));
child.on('stderr', (chunk) => process.stderr.write(chunk));
const result = await child;
process.exitCode = result.code ?? 1;Attach listeners before awaiting. The wrapper still buffers every chunk, so this is not safe for commands with unlimited output.
Run in a specific working directorypass-working-directory
const result = await spawn('npm', ['test'], {
cwd: '/srv/my-project',
env: process.env,
});The third argument is forwarded to `cross-spawn`; the package README does not enumerate these standard spawn options.
Extend the child environmentadd-environment-variable
await spawn('node', ['script.js'], {
env: {
...process.env,
NODE_ENV: 'production',
},
});Spreading `process.env` matters. Supplying only your custom key discards PATH and other environment values the command may need.
Let an interactive command use the terminalinherit-terminal-stdio
const { code, stdout, stderr } = await spawn('npm', ['init'], {
stdio: 'inherit',
});
console.log(code, stdout.length, stderr.length);With inherited stdio, stdout and stderr are not piped, so the returned Buffers are empty and no output events are emitted.
Distinguish a spawn error from an exit failurehandle-missing-command
try {
const result = await spawn('command-that-may-not-exist', []);
if (result.code !== 0) console.error('process failed', result.code);
} catch (error) {
if (error.code === 'ENOENT') console.error('executable not found');
else throw error;
}Only the child `error` event rejects the Promise; an executable that starts and then exits 1 produces a resolved result.
Import the CommonJS package from Node ESMuse-from-esm
import spawn from 'spawndamnit';
const { code } = await spawn(process.execPath, ['--version']);
if (code !== 0) throw new Error(`node exited ${code}`);This relies on Node's CommonJS default-import interop. The package itself has no ESM export map or TypeScript declarations.
Launch the same Node executable portablyrun-current-node
const { code, stdout } = await spawn(process.execPath, [
'-e',
'process.stdout.write(process.version)',
]);
console.log(code, stdout.toString());Use `process.execPath` rather than assuming the executable is named `node` or available through the child's PATH.
Pass user data as an argument, not shell textavoid-shell-injection
const branch = userSuppliedBranch;
const { code } = await spawn('git', ['show', '--stat', '--', branch], {
shell: false,
});Keep `shell` disabled for untrusted values. Argument arrays avoid shell interpolation, while `--` stops Git from treating the value as an option.
Parse JSON output after a successful exitdecode-json-output
const result = await spawn('npm', ['view', 'react', '--json']);
if (result.code !== 0) {
throw new Error(result.stderr.toString('utf8'));
}
const metadata = JSON.parse(result.stdout.toString('utf8'));stdout and stderr are Buffers, not strings. Decode explicitly, and only parse after checking the exit code.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| execa | npm | Choose it for typed ESM, rich errors, cancellation, timeouts, piping, input handling, and configurable output limits |
| zx | npm | Choose it for script-like automation where a shell-oriented template API and bundled utilities improve readability |
| cross-spawn | npm | Choose it when you only need Windows-correct spawning and want to keep the real ChildProcess handle |