child_process
require('node:child_process') The node:child_process module (also exposed as child_process) lets you spawn subprocesses, run shell commands, and launch additional Node.js processes with an IPC channel. The module is marked 2 - Stable and is built into Node.js — no install required. exec, execFile, and fork are all implemented on top of spawn (or spawnSync for the sync variants), so picking the right factory is mostly a question of how you want output handled and whether you need a shell.
const { spawn } = require('node:child_process');
// or
import { spawn } from 'node:child_process';
Asynchronous process creation
The async factories all return a ChildProcess instance. They differ in whether they spawn a shell, how they handle output, and what they return.
child_process.spawn(command[, args][, options])
Added in v0.1.90. The primary API: spawns a process directly (no shell unless you opt in) and streams stdout/stderr instead of buffering them. Use it for long-running processes or any output you want to consume incrementally.
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
// stdout: <directory listing>
// stderr:
// child process exited with code 0
SpawnOptions accepts the most knobs of any factory. The ones you will reach for most often:
| Option | Type | Default | Notes |
|---|---|---|---|
cwd | string | URL | inherits | file: URL supported since v16.4.0. Throws ENOENT if the path or command is missing. |
env | object | process.env | Keys with undefined values are ignored. |
stdio | Array | string | 'pipe' | See options.stdio below. |
shell | boolean | string | false | Pass true to use /bin/sh on Unix or process.env.ComSpec on Windows. Since v23.11.0, passing args together with shell: true is deprecated. |
detached | boolean | false | See options.detached and unref(). |
signal | AbortSignal | — | Added v15.5.0. Pass an AbortController’s signal; calling controller.abort() delivers killSignal to the child. |
timeout | number | — | Added v15.13.0. After this many ms, killSignal is delivered. |
killSignal | string | number | 'SIGTERM' | Signal used by timeout and signal. |
windowsHide | boolean | false | Added v8.8.0. Hides the console window on Windows. |
serialization | 'json' | 'advanced' | 'json' | Added v13.2.0. 'advanced' uses v8.serialize for Map, Set, BigInt, typed arrays, etc. |
spawn does not buffer output. If you do not consume stdout and the child writes more than the OS pipe buffer can hold, the child will block. Either pipe the data somewhere (a file, the parent’s stdout) or set stdio: 'ignore' if you do not care.
child_process.exec(command[, options][, callback])
Added in v0.1.90. Spawns a shell (/bin/sh on Unix, cmd.exe on Windows), runs the command string inside it, and buffers the output up to maxBuffer (default 1 MiB). The command string goes through the shell, so pipes, redirects, and globbing all work — but you become responsible for quoting and escaping.
const { exec } = require('node:child_process');
exec('cat *.js missing_file | wc -l', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
// stdout: 0
// stderr: cat: missing_file: No such file or directory
The callback signature is (error, stdout, stderr). On success error is null; on failure it is an Error with .code set to the exit code and .signal set when killed by a signal. Any non-zero exit is treated as an error by convention.
The promisified form resolves to { stdout, stderr } and rejects with an Error that also has .stdout and .stderr attached:
const { promisify } = require('node:util');
const exec = promisify(require('node:child_process').exec);
const { stdout } = await exec('ls');
console.log('stdout:', stdout);
Useful ExecOptions beyond what spawn accepts: encoding (default 'utf8'; use 'buffer' for raw Buffer output), maxBuffer (default 1 MiB), and timeout (default 0 — no timeout).
child_process.execFile(file[, args][, options][, callback])
Added in v0.1.91. Same as exec but spawns the executable directly — no shell unless you set shell: true. This is the safer pick when you do not need shell features, because you pass arguments as an array and there is no interpreter to escape.
const { execFile } = require('node:child_process');
execFile('node', ['--version'], (error, stdout) => {
if (error) throw error;
console.log(stdout);
});
// v22.x.x
On Windows, .bat and .cmd files are not executable on their own without a terminal, so they cannot be launched with execFile. Use exec (which goes through cmd.exe) or spawn('cmd.exe', ['/c', 'my.bat']) instead.
child_process.fork(modulePath[, args][, options])
Added in v0.5.0. Spawns a fresh Node.js process and runs modulePath inside it, with an IPC channel already set up. The parent uses child.send() and listens for 'message'; the child uses process.send() and 'message' on its own process object.
Parent (parent.js):
const { fork } = require('node:child_process');
const child = fork(`${__dirname}/sub.js`);
child.on('message', (m) => console.log('PARENT got message:', m));
child.send({ hello: 'world' });
The child file is a standalone Node module. It registers a 'message' listener on its own process object — that is the IPC counterpart the parent is talking to — and then sends a payload back. The response includes NaN, which the default 'json' serialiser has no way to represent, so the parent sees null instead of NaN.
Child (sub.js):
process.on('message', (m) => console.log('CHILD got message:', m));
process.send({ foo: 'bar', baz: NaN });
// PARENT got message: { foo: 'bar', baz: null } (NaN serialised as null)
fork-specific options worth knowing:
execPath— defaults toprocess.execPath. Use this to launch a different Node binary.execArgv— defaults toprocess.execArgv. Pass['--inspect']to enable the debugger on the child.silent— legacy option. Iftrue, pipes the child’s stdio to the parent. Preferstdio: 'inherit'for clarity.serialization— defaults to'json'. Set to'advanced'to sendMap/Set/BigInt/typed arrays through IPC.stdio— since v6.4.0, defaults to'pipe'. Set to'ipc'plus your desired stdio config if you want both IPC and a specific I/O setup.
modulePath accepts a WHATWG file: URL since v17.4.0.
Synchronous process creation
The sync variants block the event loop until the child exits. They exist for one-shot scripts and startup-time config where async is overkill. All three throw on a non-zero exit code or a timeout — wrap them in try/catch and inspect err.status, err.signal, and (for the buffered ones) err.stdout/err.stderr.
const { execFileSync } = require('node:child_process');
try {
const stdout = execFileSync('my-script.sh', ['my-arg'], {
stdio: 'pipe',
encoding: 'utf8',
});
console.log(stdout);
} catch (err) {
if (err.code) {
console.error(err.code); // spawn failure
} else {
const { stdout, stderr } = err; // non-zero exit
console.error({ stdout, stderr });
}
}
| Method | Added | Shell? | Returns | Throws on non-zero? |
|---|---|---|---|---|
spawnSync | v0.11.12 | No | SpawnSyncReturns object | No — check .status and .error |
execSync | v0.11.12 | Yes | Buffer | string | Yes |
execFileSync | v0.11.12 | No | Buffer | string | Yes |
spawnSync returns { pid, output, stdout, stderr, status, signal, error } and lets you inspect the exit without catching. execSync and execFileSync return the buffered output directly. All three accept an input option (string | Buffer | TypedArray | DataView) for piping data into the child’s stdin.
A subtle gotcha with spawnSync timeouts: if the child intercepts SIGTERM and does not exit, the parent still waits for the child to die on its own. The method does not return until the process is fully reaped.
The ChildProcess class
ChildProcess extends EventEmitter and is what every async factory returns. The docs are explicit: do not construct one directly with new ChildProcess() — use one of the factories.
Events
| Event | Payload | Notes |
|---|---|---|
'spawn' | — | Added v15.1.0. Fires once when the child is successfully created. If spawn fails, 'error' fires instead. |
'error' | Error | Spawn failed, the child could not be killed, or sending a message failed. Also fires when the child is aborted via signal. |
'exit' | (code: number | null, signal: string | null) | Process has ended; stdio streams may still be open. One of code/signal is always non-null. |
'close' | (code, signal) | Stdio streams have all closed. Distinct from 'exit' because multiple processes can share stdio. Always fires after 'exit'. |
'message' | (message, sendHandle) | Received via IPC from the child. message is the parsed JSON (or richer with serialization: 'advanced'). |
'disconnect' | — | Fires after .disconnect() on either side, or when the IPC channel closes. |
Properties
| Property | Type | Notes |
|---|---|---|
subprocess.pid | number | undefined | OS PID. undefined if spawn failed. |
subprocess.connected | boolean | false after disconnect(). |
subprocess.exitCode | number | null | null while running or if killed by signal. |
subprocess.signalCode | string | null | Set when killed by a signal. |
subprocess.killed | boolean | true once kill() delivered its signal. Does not mean the process is dead. |
subprocess.spawnfile | string | Executable name (e.g. process.execPath for fork, /bin/sh for exec). |
subprocess.spawnargs | string[] | Full args passed to the child. |
subprocess.stdin | Writable | null | undefined | Alias of stdio[0]. null/undefined if spawn failed or stdio is not piped. |
subprocess.stdout | Readable | null | undefined | Alias of stdio[1]. |
subprocess.stderr | Readable | null | undefined | Alias of stdio[2]. |
subprocess.stdio | Array | Sparse array of pipes, one per fd where stdio is 'pipe'. |
subprocess.channel | Object | undefined | IPC channel reference. undefined if no IPC was set up. |
Methods
subprocess.kill([signal])— defaults to'SIGTERM'. Returns aboolean(success ofkill(2)). On Windows onlySIGKILL,SIGTERM,SIGINT, andSIGQUITare honoured. The signal may not actually terminate the process.subprocess.send(message[, sendHandle[, options]][, callback])— IPC only (set up automatically byfork). Returns abooleanfor backpressure. Reservedcmdvalues starting withNODE_are internal.subprocess.disconnect()— close the IPC channel so the child can exit gracefully.subprocess.ref()/subprocess.unref()— added v0.7.10.unref()lets the parent’s event loop exit even if the child is still running.subprocess[Symbol.dispose]— stable in v24.2.0. Calling it triggerskill('SIGTERM'). Use withusing proc = spawn(...)for scoped cleanup.
Tying events, properties, and methods together — spawn a long-running child, observe its lifecycle, read its state, and stop it on a timer:
const { spawn } = require('node:child_process');
const child = spawn('node', ['-e', 'setInterval(() => {}, 1000)']);
child.on('spawn', () => {
console.log(`spawned with pid ${child.pid}`);
});
child.on('exit', (code, signal) => {
console.log(`exited: code=${code} signal=${signal}`);
});
child.on('close', (code, signal) => {
console.log(`stdio closed: code=${code} signal=${signal}`);
});
setTimeout(() => {
child.kill('SIGTERM');
}, 5000);
child.unref();
// spawned with pid 12345
// exited: code=null signal=SIGTERM
// stdio closed: code=null signal=SIGTERM
options.stdio
The stdio option controls how the child’s three standard streams (and optionally an IPC channel) are wired up. The default is 'pipe', which gives you subprocess.stdin, subprocess.stdout, and subprocess.stderr as Node streams.
String shortcuts:
| String | Equivalent |
|---|---|
'pipe' | ['pipe', 'pipe', 'pipe'] (default) |
'overlapped' | ['overlapped', 'overlapped', 'overlapped'] — Windows only; needed for overlapped I/O |
'ignore' | ['ignore', 'ignore', 'ignore'] — opens /dev/null for each |
'inherit' | [process.stdin, process.stdout, process.stderr] |
Array element values:
| Value | Meaning |
|---|---|
'pipe' | Create a pipe; the parent reads/writes via child.stdio[fd]. |
'overlapped' | Like 'pipe' with FILE_FLAG_OVERLAPPED on Windows; identical to 'pipe' elsewhere. |
'ipc' | Set up an IPC channel. At most one per ChildProcess. |
'ignore' | Open /dev/null and attach. |
'inherit' | Pass through the parent’s corresponding stdio. Positions 0–2 only. |
<Stream> | Share a readable/writable stream (e.g. a TTY, a file). The stream must have an open underlying fd. |
| Positive integer | Reuse a parent’s already-open fd. Sockets are not supported on Windows. |
null / undefined | Default value for that position. |
A common pattern redirects output to log files using fs.openSync() to obtain a file descriptor:
const { openSync } = require('node:fs');
const { spawn } = require('node:child_process');
const out = openSync('./out.log', 'a');
const err = openSync('./out.log', 'a');
const proc = spawn('prg', [], {
stdio: ['ignore', out, err],
});
options.detached and unref()
detached: true changes how the child relates to its parent. On Windows, the child gets its own console window and can outlive the parent. On Unix, the child becomes a session leader via setsid(2) and can keep running after the parent exits — but only if you also call child.unref() and give it a stdio setup that is not connected to the parent.
The classic “fully detached background process” pattern:
const { openSync } = require('node:fs');
const { spawn } = require('node:child_process');
const out = openSync('./out.log', 'a');
const err = openSync('./out.log', 'a');
const subprocess = spawn('prg', [], {
detached: true,
stdio: ['ignore', out, err],
});
subprocess.unref(); // Parent can exit; child keeps running.
Without unref(), the child’s stdio pipes (even when pointing at files) keep the parent’s event loop alive. Without detached: true, killing the parent may also kill the child via the process group.
Common gotchas
- Shell injection.
execalways uses a shell.execFileandspawnuse one only if you setshell: true. Never concatenate user input — pass arguments as discrete array elements. - Pipe buffer deadlock. By default stdio is piped with a finite OS-level buffer. If the child writes to
stdoutfaster than the parent reads, the child blocks. Stream the data, redirect to a file/TTY, or setstdio: 'ignore'. - Windows
.bat/.cmd.execFilewill not launch them. Useexecorspawn('cmd.exe', ['/c', 'my.bat']). maxBufferis 1 MiB by default forexec,execFile, and the sync variants. A child that prints more is killed and the output truncated. Raise the limit or stream the output.maxBuffer+ multi-byte characters. A buffer threshold that splits a UTF-8 character kills the child. Pick a size that fits your largest expected character width, or stream.kill()is best-effort. It delivers a signal; the child can ignore it. On Windows, onlySIGKILL,SIGTERM,SIGINT, andSIGQUITare honoured.subprocess.killed === trueonly means the signal was delivered, not that the process is gone.- Sync methods throw on non-zero exit or timeout. Wrap in
try/catch. The thrownErrorcarries.status,.signal, and (for the buffered ones).stdout/.stderr. - Killing a child does not kill its grandchildren. A shell that wraps Node, for example, will keep running unless you also kill the process group or use a
tree-kill-style helper. - IPC socket handles are not supported on Windows.
child.send(message, socket)with anet.Socketwill not work there. forkdoes not run CLI parsing. It loads the file as a module, so flags like--inspectneed to come throughexecArgv.serialization: 'advanced'(v13.2.0+) is the way to sendMap/Set/BigInt/typed arrays over IPC. Default'json'serialisation turnsNaNintonulland dropsundefined.stdio: 'overlapped'is needed on Windows for asynchronous I/O on the child’s stdio. It behaves like'pipe'on every other platform.
Summary
The child_process module is how Node.js spawns subprocesses, runs shell commands, and launches child Node processes. Reach for spawn when you want to stream output from a long-running command; use exec for short shell one-liners that fit in a buffer. Switch to execFile when you want to skip the shell and pass arguments as an array. Use fork to launch another Node script with IPC. The sync variants (spawnSync, execSync, execFileSync) exist for startup scripts and one-shot tasks, but they block the event loop, so keep them out of request handlers. Always stream the output, sanitise any user input, and remember that kill() only sends a signal — it does not guarantee the process is gone.