Run an external program from Node.js
Choose execFile for argument-based execution, spawn for streams, or exec for a deliberate shell command, and handle startup and exit failures.
Use execFile() to run an executable with an argument array, or spawn() when you want to stream its output. Use exec() only when shell interpretation is intentional. The synchronous variants block the calling JavaScript thread.
Run a deliberate shell command
On a POSIX system:
const { exec } = require('node:child_process');
exec('ls -la /tmp', (error, stdout, stderr) => {
if (error) {
console.error(error.message);
return;
}
process.stdout.write(stdout);
});
The shell handles pipes, redirections, and expansion. Never concatenate untrusted text into such a command. exec() buffers stdout and stderr; the current documented default maxBuffer is 1 MiB for each. Exceeding it can terminate the child and truncate output.
Pass arguments directly
This example launches the same Node executable that is running the parent:
const { execFile } = require('node:child_process');
execFile(process.execPath, ['--version'], (error, stdout, stderr) => {
if (error) {
console.error(error.message);
return;
}
process.stdout.write(stdout);
});
execFile() does not use a shell by default. An installed image tool can be invoked the same way, for example with arguments ['input.png', '-resize', '50%', 'output.png'], using its actual executable path and supported syntax.
An argument array avoids shell expansion, but does not make the target program safe for arbitrary input. Validate executable selection and arguments; an input beginning with - may become an option. Use an option terminator such as -- only where that program supports it. Enabling shell: true changes the safety model.
Stream a long-running command
With FFmpeg installed and an existing input file:
const { spawn } = require('node:child_process');
const child = spawn('ffmpeg', ['-i', 'input.mp4', 'output.webm']);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
child.on('error', (error) => console.error('Could not start:', error.message));
child.on('close', (code, signal) => {
console.log(`Child closed: code=${code}, signal=${signal}`);
});
Piping handles stream backpressure. If you collect every chunk into an array instead, memory still grows with the output. Handle startup errors as well as nonzero exit codes; a close event also indicates the child's stdio streams have closed.
Use synchronous execution in a bounded script
Inside a Git repository:
const { execSync } = require('node:child_process');
const revision = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
console.log(revision);
This waits and can throw on failure. It is convenient in some build scripts but blocks request handling if used in a server's active path.
| Requirement | Starting point |
|---|---|
| Intentional shell syntax | exec |
| Executable plus separate arguments | execFile |
| Incremental output or a long-running child | spawn |
| Deliberately blocking script | A synchronous variant |
FAQ
Why does spawn report ENOENT? The executable may be missing from PATH, but the configured working directory may also not exist. Check both. Turning on a shell is not a general fix for a missing executable.
How do I avoid the output buffer limit? Use streams for large output, or choose a justified maxBuffer for bounded output. Remember that the limit counts bytes, not characters.
What about Windows .cmd and .bat files? They need a command interpreter rather than direct executable loading. Follow the Node Windows guidance for cmd.exe or a deliberate shell invocation, and keep untrusted data out of shell command text.
References
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan