Question
I am porting a CLI library from Ruby to Node.js, and part of the code needs to run third-party command line binaries when required.
For example, in Ruby I can run PrinceXML to convert an HTML file into a PDF like this:
cmd = system("prince -v builds/pdf/book.html -o builds/pdf/book.pdf")
What is the equivalent approach in Node.js for executing a command line binary?
Short Answer
By the end of this page, you will understand how to run external command line programs from Node.js using the child_process module. You will learn the difference between exec, spawn, and execFile, when to use each one, how to pass arguments safely, and how to handle success, failure, and command output in real CLI applications.
Concept
In Node.js, external programs are usually executed with the built-in child_process module. This module lets your JavaScript code start another process, pass it arguments, read its output, and detect whether it succeeded or failed.
This matters because many real-world tools are not written in JavaScript. A Node.js application may need to call:
- PDF generators like PrinceXML
- Git commands
- Image tools like ImageMagick
- Shell utilities
- Build tools and compilers
The main idea is simple:
- Node.js runs your JavaScript process
- Your JavaScript starts another program
- That program does its work and returns output or an exit code
Node provides a few main ways to do this:
exec()— runs a command through a shell and collects the outputspawn()— starts a process directly and streams output as it runsexecFile()— runs a file directly without a shellspawnSync()/execSync()— synchronous versions, usually better for simple scripts than for servers
For most CLI wrappers, the safest and most common choices are:
spawn()for long-running commands or large outputexecFile()when you want to run a binary with arguments safely
If you are translating code like Ruby's system(...), you are usually trying to run a command and check whether it succeeded. In Node, you often do that by listening for the process exit event or by checking the callback error.
Mental Model
Think of your Node.js program as a manager in an office.
- Your main script is the manager
- A command line binary like
princeis a specialist worker child_processis the act of calling that worker into a room and giving them instructions
You can interact with that worker in different ways:
- With
exec(), you say: "Run this whole shell command and bring me back the final result." - With
spawn(), you say: "Start working now, and keep reporting progress while you work." - With
execFile(), you say: "Run this exact program with these exact arguments."
If your command is simple and trusted, exec() can be convenient. If you want more control, safer argument handling, or streaming output, spawn() or execFile() is usually a better fit.
Syntax and Examples
Basic option: exec()
const { exec } = require('child_process');
exec('prince -v builds/pdf/book.html -o builds/pdf/book.pdf', (error, stdout, stderr) => {
if (error) {
console.error('Command failed:', error.message);
return;
}
if (stdout) {
console.log('stdout:', stdout);
}
if (stderr) {
console.log('stderr:', stderr);
}
console.log('PDF created successfully');
});
This is the closest style to the Ruby example. It runs a command string in a shell.
Safer option: spawn()
const { spawn } = require('child_process');
const child = spawn('prince', ['-v', 'builds/pdf/book.html', '-o', 'builds/pdf/book.pdf']);
child.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
child.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
child.on('close', (code) => {
if (code === 0) {
console.log('PDF created successfully');
} else {
console.error(`Process exited with code ${code}`);
}
});
Here, the command and its arguments are separated. This avoids shell parsing issues and is usually a better choice.
Best fit for many binaries: execFile()
const { execFile } = require('child_process');
execFile(
'prince',
['-v', 'builds/pdf/book.html', '-o', 'builds/pdf/book.pdf'],
(error, stdout, stderr) => {
if (error) {
console.error('Command failed:', error.message);
return;
}
console.log('stdout:', stdout);
console.log('stderr:', stderr);
console.log('PDF created successfully');
}
);
execFile() is similar to exec(), but it runs a specific executable directly instead of passing a full command string to a shell.
Which one should a beginner choose?
- Use
exec()for quick, simple, trusted shell commands - Use
execFile()when calling a known binary with argument lists - Use
spawn()when output may be large or you want streaming behavior
Step by Step Execution
Example
const { spawn } = require('child_process');
const child = spawn('prince', ['-v', 'input.html', '-o', 'output.pdf']);
child.stderr.on('data', (data) => {
console.log(data.toString());
});
child.on('close', (code) => {
console.log('Exit code:', code);
});
What happens step by step
- Node loads the
spawnfunction from thechild_processmodule. spawn('prince', ['-v', 'input.html', '-o', 'output.pdf'])starts theprinceprogram.- Node creates a child process for that external binary.
- The
princeprocess begins converting the HTML file to a PDF. - If
princewrites status messages or errors to standard error, thedataevent runs. dataarrives as aBuffer, sotoString()converts it into readable text.- When the process finishes, the
closeevent runs. - The
codetells you whether it succeeded:0usually means success- any other number usually means failure
Interpreting the result
- If the output PDF is created and the exit code is
0, the command worked. - If the binary is missing, Node may emit an error like
ENOENT. - If the binary runs but fails, you often get a non-zero exit code.
Real World Use Cases
Running external binaries from Node.js is common in many kinds of projects.
CLI tools
A Node-based CLI may wrap existing system tools:
- generate PDFs with PrinceXML
- run
gitcommands - format files with external tools
- call compilers and build systems
Web servers
A backend may trigger external programs to process uploaded files:
- convert images
- generate thumbnails
- create PDFs from HTML
- extract metadata from media files
Automation scripts
Build and deployment scripts often call shell tools:
- copy files
- compress assets
- run test commands
- deploy with system utilities
Data processing
Node scripts may use specialized native tools for performance:
- CSV conversion
- video transcoding with FFmpeg
- OCR tools
- document generation
Real Codebase Usage
In real projects, developers rarely just fire a command and ignore the result. They usually wrap command execution in a reusable helper.
Common pattern: small wrapper function
const { execFile } = require('child_process');
function runPrince(inputPath, outputPath, callback) {
execFile('prince', ['-v', inputPath, '-o', outputPath], (error, stdout, stderr) => {
if (error) {
callback(new Error(`Prince failed: ${stderr || error.message}`));
return;
}
callback(null, { stdout, stderr });
});
}
This keeps the rest of the codebase clean.
Common pattern: validation before execution
Before running a binary, developers often validate:
- input file exists
- output directory exists
- command arguments are not empty
- paths are constructed safely
Common pattern: early return on failure
if (!inputPath) {
throw new Error('Input path is required');
}
This prevents confusing process errors later.
Common pattern: Promise wrapper
Modern Node.js code often converts callbacks into Promises:
const { execFile } = require('child_process');
const { promisify } = require('util');
const execFileAsync = promisify(execFile);
async function createPdf() {
try {
const result = await execFileAsync('prince', ['-v', 'input.html', '-o', 'output.pdf']);
console.log(result.stdout);
} catch (error) {
console.error('Failed:', error.message);
}
}
This fits well in async/await code.
Common pattern: logging stderr even on success
Some tools write useful status messages to stderr even when they succeed. Developers often inspect both stdout and stderr, not just one of them.
Common Mistakes
1. Using exec() with untrusted user input
Broken example:
const { exec } = require('child_process');
const filename = userInput;
exec(`prince ${filename} -o output.pdf`);
Why this is a problem:
exec()runs through a shell- shell metacharacters in user input can change the command
- this can create command injection vulnerabilities
Safer approach:
const { execFile } = require('child_process');
execFile('prince', [userInput, '-o', 'output.pdf'], (error) => {
if (error) {
console.error(error.message);
}
});
2. Forgetting to handle errors
Broken example:
const { exec } = require('child_process');
exec('prince input.html -o output.pdf');
Why this is a problem:
- if the binary is missing or fails, you may never notice properly
- debugging becomes difficult
Better:
exec('prince input.html -o output.pdf', (error, stdout, stderr) => {
if (error) {
console.error(error);
}
});
3. Assuming stderr always means failure
Some binaries write logs or warnings to stderr even when they succeed.
Always check:
- the actual error object
- the exit code
- whether the expected output file was created
4. Using exec() for very large output
exec() buffers output in memory. If the command prints a lot, this can fail.
Use spawn() instead when output is large or continuous.
5. Not separating command and arguments
Less safe:
exec('prince -v input.html -o output.pdf');
Better for many cases:
spawn('prince', ['-v', 'input.html', '-o', 'output.pdf']);
6. Ignoring platform differences
Some commands work differently across operating systems. If your Node app depends on a binary, make sure:
- the binary is installed
- it is available in the system
PATH - paths are built carefully
Comparisons
exec() vs spawn() vs execFile()
| Method | Uses a shell | Output style | Best for | Notes |
|---|---|---|---|---|
exec() | Yes | Buffered | Simple command strings | Convenient, but less safe with user input |
spawn() | No by default | Streamed | Long-running commands, large output | More control over process I/O |
execFile() | No | Buffered | Running a known executable with arguments | Often the safest simple choice |
exec() vs Ruby system
| Ruby | Node.js equivalent | Notes |
|---|---|---|
system("command") | exec("command", callback) | Similar feel, runs a command string |
system("binary", "arg1", "arg2") style | spawn("binary", ["arg1", "arg2"]) or execFile(...) | Better argument handling |
Asynchronous vs synchronous versions
| Version | Behavior | When to use |
|---|---|---|
exec() / spawn() / execFile() | Non-blocking | Most applications |
execSync() / spawnSync() | Blocks until finished | Small scripts or setup tasks |
In most production Node.js apps, asynchronous versions are preferred because they do not block the event loop.
Cheat Sheet
Core module
const { exec, spawn, execFile } = require('child_process');
Quick examples
Run a shell command
exec('prince input.html -o output.pdf', (error, stdout, stderr) => {
if (error) return console.error(error.message);
});
Run a binary with argument array
spawn('prince', ['input.html', '-o', 'output.pdf']);
Run a binary directly and get callback result
execFile('prince', ['input.html', '-o', 'output.pdf'], (error, stdout, stderr) => {
if (error) return console.error(error.message);
});
Rules of thumb
- Use
exec()for small trusted command strings - Use
execFile()for known binaries with explicit arguments - Use
spawn()for large output or streaming - Check
error,stderr, and exit code - Avoid putting raw user input into
exec()strings
Common errors
ENOENT→ binary not found- non-zero exit code → command ran but failed
- empty output → command may still have succeeded
Exit code meaning
0usually means success- non-zero usually means failure
FAQ
How do I run a terminal command in Node.js?
Use the built-in child_process module, usually with exec(), spawn(), or execFile().
What is the Node.js equivalent of Ruby system()?
The closest equivalent is often exec() for simple command strings, though spawn() or execFile() is usually better for safer argument handling.
Should I use exec() or spawn() in Node.js?
Use exec() for simple commands with small output. Use spawn() when you need streaming output, more control, or expect a lot of output.
What happens if the binary is not installed?
Node will usually report an error such as ENOENT, which means it could not find the executable.
Is exec() safe with user input?
Not usually. If user input is inserted into a shell command string, it can lead to command injection. Prefer spawn() or execFile() with argument arrays.
Can I wait for a command to finish in Node.js?
Yes. With callback-based APIs, use the callback or process events. With Promises, wrap the function and use await.
Does stderr always mean the command failed?
No. Some programs write logs, warnings, or progress messages to stderr even when they succeed.
Can Node.js run command line tools like PrinceXML, Git, or FFmpeg?
Yes. As long as the binary is installed and accessible on the system, Node can execute it using child_process.
Mini Project
Description
Build a small Node.js utility that converts an HTML file into a PDF by calling the prince binary. This demonstrates how to execute an external command, pass arguments safely, and report success or failure clearly.
Goal
Create a script that runs PrinceXML from Node.js and prints a clear message based on the process result.
Requirements
- Accept an input HTML file path and an output PDF file path
- Run the
princebinary with those paths as arguments - Print any command output or errors to the console
- Exit with a success message only when the command finishes successfully
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.