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 , you are usually trying to . 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', , , ]);
child..(, {
.();
});
child..(, {
.();
});
child.(, {
(code === ) {
.();
} {
.();
}
});
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, the event runs.
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 ();
}
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');
();
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 |
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.);
});
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 with argument arrays.
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
How to Call Shell Commands from Ruby and Capture Output
Learn how to run shell commands in Ruby, capture output, check exit status, and choose the right method for scripts and apps.
How to Check Whether a String Contains a Substring in Ruby
Learn how to check if a string contains a substring in Ruby using include?, match, and multiline string examples.
How to Check if a Hash Key Exists in Ruby
Learn how to check whether a specific key exists in a Ruby hash using key?, has_key?, and include? with clear examples.