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.

Best practices
Illustration of an event loop and asynchronous tasks

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

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting