child_process.exec()

Run a command through a shell and pass its buffered stdout and stderr to a callback.

Signature
child_process.exec(command[, options][, callback])
  • Added in Node.js v0.1.90

Parameters

ParameterTypeDescription
$command<string>The command to run, with space-separated arguments. Interpreted by a shell, so pipes, redirects and globs work.
$optionsoptional<Object>Execution options.
$options.cwdoptional<string> | <URL>= process.cwd()Current working directory of the child process.
$options.envoptional<Object>= process.envEnvironment key-value pairs, replacing the child's environment entirely.
$options.encodingoptional<string>= 'utf8'Encoding used to decode stdout and stderr. Use 'buffer' to receive Buffers.
$options.shelloptional<string>= '/bin/sh' on Unix, process.env.ComSpec on WindowsShell to execute the command with.
$options.signaloptional<AbortSignal>Allows aborting the child process using an AbortSignal.
$options.timeoutoptional<number>= 0Milliseconds after which the child is sent killSignal. 0 disables the timeout.
$options.maxBufferoptional<number>= 1024 * 1024Largest amount of data in bytes allowed on stdout or stderr before the child is killed.
$options.killSignaloptional<string> | <integer>= 'SIGTERM'Signal used when the child is killed by timeout or maxBuffer overflow.
$options.uidoptional<number>Sets the user identity of the process (see setuid(2)).
$options.gidoptional<number>Sets the group identity of the process (see setgid(2)).
$options.windowsHideoptional<boolean>= falseHide the subprocess console window that would normally be created on Windows.
$callbackoptional<Function>Called with (error, stdout, stderr) when the process terminates. error is null on a zero exit code.

Return value

<ChildProcess>A ChildProcess instance. The buffered output is delivered to the callback rather than through the return value.

Errors

Does not throw for a failing command; a non-zero exit code, a timeout, or exceeding maxBuffer is reported as the error argument to the callback, carrying code, signal, killed and cmd properties.

Changelog

  • PHP v16.4.0, v14.18.0The cwd option can be a WHATWG URL object using the file: protocol.
  • PHP v15.4.0AbortSignal support was added via the signal option.
  • PHP v8.8.0The windowsHide option is supported.

exec() hands the whole string to /bin/sh -c (or cmd.exe /d /s /c) and buffers everything the command prints, then calls you back once with the complete text.

When to reach for it

Reach for it when you genuinely want shell features and the output is small: a pipeline, a glob, an environment expansion, git rev-parse HEAD. It also pairs neatly with util.promisify, which resolves to { stdout, stderr }.

What trips people up

  • Command injection. Any interpolated value is shell syntax. exec(`grep ${term} file`) with term = 'x; rm -rf .' runs both commands. If the input is not a literal you wrote, use execFile().
  • maxBuffer. Output over 1 MiB kills the child and the callback receives ERR_CHILD_PROCESS_STDIO_MAXBUFFER with the truncated output. This is the classic "works on my small repo" bug.
  • A non-zero exit code is an error, not a normal result, so a command that legitimately exits 1 (like grep finding nothing) lands in your error branch. Inspect error.code.
  • stderr is not an error signal. Plenty of tools write progress there while succeeding.

For anything long-running or noisy, use spawn() instead.

Example

Example · javascript
const { exec } = require('node:child_process');

// The string goes to a shell, so && and redirection work.
exec('echo hello && echo oops 1>&2', { encoding: 'utf8' }, (error, stdout, stderr) => {
  console.log('error:', error);
  console.log('stdout:', JSON.stringify(stdout));
  console.log('stderr:', JSON.stringify(stderr));
});

// error: null
// stdout: "hello\n"
// stderr: "oops\n"

When to use it

  • Read a single value out of a CLI tool, such as the current git commit hash.
  • Run a short shell pipeline that would be awkward to reimplement in JavaScript.
  • Call a system utility during a build script and await it with util.promisify.

Frequently asked questions

How do I run a shell command in Node.js and get the output?

Call child_process.exec(command, callback) and read the stdout argument in the callback, or wrap it with util.promisify to await a { stdout, stderr } object. Keep the output under the 1 MiB maxBuffer default, and switch to spawn() if the command can print more than that.

Why does exec return an error when the command worked?

exec() reports any non-zero exit status as an error, so tools that use exit codes to mean something other than failure (grep finding no match, diff finding differences) land in the error branch. Check error.code against the exit codes the tool documents instead of treating every error as a crash.

Is child_process.exec safe with user input?

No. The command string is interpreted by a shell, so any interpolated value can inject extra commands with ; or &&. Use execFile() with an arguments array, which passes each argument to the program untouched.

More examples

Awaiting a command with util.promisify

The promisified form resolves to an object with stdout and stderr, and rejects with the same Error the callback would have received. The rejection still carries code, killed and cmd, which is how you tell a failed command (code 4) from one that was killed by a timeout (killed: true).

Example · javascript
import { exec } from 'node:child_process';
import { promisify } from 'node:util';

const run = promisify(exec);

const { stdout } = await run('echo 40 && echo 2');
console.log('lines:', stdout.trim().split('\n'));

try {
  await run('exit 4');
} catch (error) {
  // The rejection value is an Error carrying the child's result.
  console.log('code:', error.code);
  console.log('killed:', error.killed);
  console.log('cmd:', error.cmd);
}

// lines: [ '40', '2' ]
// code: 4
// killed: false
// cmd: exit 4

Handling the maxBuffer limit

Once the child writes past maxBuffer, Node kills it and calls back with ERR_CHILD_PROCESS_STDIO_MAXBUFFER. Note that stdout is still delivered but truncated to the limit, so silently ignoring the error gives you a partial result that looks valid. The real default is 1 MiB; 100 bytes is used here to make the failure visible.

Example · javascript
const { exec } = require('node:child_process');

// Deliberately produce more output than the tiny limit allows.
exec('node -e "console.log(\'x\'.repeat(5000))"', { maxBuffer: 100 }, (error, stdout) => {
  console.log('error code:', error.code);
  console.log('killed:', error.killed);
  console.log('stdout length:', stdout.length);
});

// error code: ERR_CHILD_PROCESS_STDIO_MAXBUFFER
// killed: true
// stdout length: 100

Why interpolation is unsafe

The exec() output contains INJECTED on its own line, proving the shell split the string into two commands. execFile() passes the same text as a single argv entry, so the semicolon is just a character in the filename. This is the whole argument for preferring execFile() whenever any part of the command comes from outside your source code.

Example · javascript
const { exec, execFile } = require('node:child_process');

const userInput = 'notes.txt; echo INJECTED';

// Unsafe: the semicolon is shell syntax, so a second command runs.
exec(`echo reading ${userInput}`, (err, stdout) => {
  console.log('exec  ->', JSON.stringify(stdout));
});

// Safe: the argument is passed to the program verbatim, never parsed.
execFile('echo', ['reading', userInput], (err, stdout) => {
  console.log('execFile ->', JSON.stringify(stdout));
});

// exec  -> "reading notes.txt\nINJECTED\n"
// execFile -> "reading notes.txt; echo INJECTED\n"

Discussion

  • Be the first to comment on this lesson.