child_process.exec()
Run a command through a shell and pass its buffered stdout and stderr to a callback.
child_process.exec(command[, options][, callback])- Added in Node.js v0.1.90
Parameters
| Parameter | Type | Description |
|---|---|---|
$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.env | Environment 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 Windows | Shell to execute the command with. |
$options.signaloptional | <AbortSignal> | Allows aborting the child process using an AbortSignal. |
$options.timeoutoptional | <number>= 0 | Milliseconds after which the child is sent killSignal. 0 disables the timeout. |
$options.maxBufferoptional | <number>= 1024 * 1024 | Largest 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>= false | Hide 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`)withterm = '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 (likegrepfinding nothing) lands in your error branch. Inspecterror.code. stderris not an error signal. Plenty of tools write progress there while succeeding.
For anything long-running or noisy, use spawn() instead.
Example
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).
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 4Handling 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.
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: 100Why 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.
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