jsguides

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:

OptionTypeDefaultNotes
cwdstring | URLinheritsfile: URL supported since v16.4.0. Throws ENOENT if the path or command is missing.
envobjectprocess.envKeys with undefined values are ignored.
stdioArray | string'pipe'See options.stdio below.
shellboolean | stringfalsePass 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.
detachedbooleanfalseSee options.detached and unref().
signalAbortSignalAdded v15.5.0. Pass an AbortController’s signal; calling controller.abort() delivers killSignal to the child.
timeoutnumberAdded v15.13.0. After this many ms, killSignal is delivered.
killSignalstring | number'SIGTERM'Signal used by timeout and signal.
windowsHidebooleanfalseAdded 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 to process.execPath. Use this to launch a different Node binary.
  • execArgv — defaults to process.execArgv. Pass ['--inspect'] to enable the debugger on the child.
  • silent — legacy option. If true, pipes the child’s stdio to the parent. Prefer stdio: 'inherit' for clarity.
  • serialization — defaults to 'json'. Set to 'advanced' to send Map/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 });
  }
}
MethodAddedShell?ReturnsThrows on non-zero?
spawnSyncv0.11.12NoSpawnSyncReturns objectNo — check .status and .error
execSyncv0.11.12YesBuffer | stringYes
execFileSyncv0.11.12NoBuffer | stringYes

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

EventPayloadNotes
'spawn'Added v15.1.0. Fires once when the child is successfully created. If spawn fails, 'error' fires instead.
'error'ErrorSpawn 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

PropertyTypeNotes
subprocess.pidnumber | undefinedOS PID. undefined if spawn failed.
subprocess.connectedbooleanfalse after disconnect().
subprocess.exitCodenumber | nullnull while running or if killed by signal.
subprocess.signalCodestring | nullSet when killed by a signal.
subprocess.killedbooleantrue once kill() delivered its signal. Does not mean the process is dead.
subprocess.spawnfilestringExecutable name (e.g. process.execPath for fork, /bin/sh for exec).
subprocess.spawnargsstring[]Full args passed to the child.
subprocess.stdinWritable | null | undefinedAlias of stdio[0]. null/undefined if spawn failed or stdio is not piped.
subprocess.stdoutReadable | null | undefinedAlias of stdio[1].
subprocess.stderrReadable | null | undefinedAlias of stdio[2].
subprocess.stdioArraySparse array of pipes, one per fd where stdio is 'pipe'.
subprocess.channelObject | undefinedIPC channel reference. undefined if no IPC was set up.

Methods

  • subprocess.kill([signal]) — defaults to 'SIGTERM'. Returns a boolean (success of kill(2)). On Windows only SIGKILL, SIGTERM, SIGINT, and SIGQUIT are honoured. The signal may not actually terminate the process.
  • subprocess.send(message[, sendHandle[, options]][, callback]) — IPC only (set up automatically by fork). Returns a boolean for backpressure. Reserved cmd values starting with NODE_ 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 triggers kill('SIGTERM'). Use with using 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:

StringEquivalent
'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:

ValueMeaning
'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 integerReuse a parent’s already-open fd. Sockets are not supported on Windows.
null / undefinedDefault 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. exec always uses a shell. execFile and spawn use one only if you set shell: 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 stdout faster than the parent reads, the child blocks. Stream the data, redirect to a file/TTY, or set stdio: 'ignore'.
  • Windows .bat / .cmd. execFile will not launch them. Use exec or spawn('cmd.exe', ['/c', 'my.bat']).
  • maxBuffer is 1 MiB by default for exec, 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, only SIGKILL, SIGTERM, SIGINT, and SIGQUIT are honoured. subprocess.killed === true only 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 thrown Error carries .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 a net.Socket will not work there.
  • fork does not run CLI parsing. It loads the file as a module, so flags like --inspect need to come through execArgv.
  • serialization: 'advanced' (v13.2.0+) is the way to send Map/Set/BigInt/typed arrays over IPC. Default 'json' serialisation turns NaN into null and drops undefined.
  • 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.

See Also

  • eventsChildProcess extends EventEmitter; the events API is the underlying mechanism
  • streamsubprocess.stdin/stdout/stderr are Writable/Readable streams
  • processprocess.send() and process.disconnect() are exposed inside forked children