Question

How to Execute a Command Line Binary in Node.js

rubyjavascriptnode.jscommand-line-interface

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 output
  • spawn() — starts a process directly and streams output as it runs
  • execFile() — runs a file directly without a shell
  • spawnSync() / 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 output
  • execFile() 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 prince is a specialist worker
  • child_process is 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.

Take Quiz

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

  1. Node loads the spawn function from the child_process module.
  2. spawn('prince', ['-v', 'input.html', '-o', 'output.pdf']) starts the prince program.
  3. Node creates a child process for that external binary.
  4. The prince process begins converting the HTML file to a PDF.
  5. If prince writes status messages or errors to standard error, the data event runs.
  6. data arrives as a Buffer, so toString() converts it into readable text.
  7. When the process finishes, the close event runs.
  8. The code tells you whether it succeeded:
    • 0 usually 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 git commands
  • 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
Take Quiz

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()

MethodUses a shellOutput styleBest forNotes
exec()YesBufferedSimple command stringsConvenient, but less safe with user input
spawn()No by defaultStreamedLong-running commands, large outputMore control over process I/O
execFile()NoBufferedRunning a known executable with argumentsOften the safest simple choice

exec() vs Ruby system

RubyNode.js equivalentNotes
system("command")exec("command", callback)Similar feel, runs a command string
system("binary", "arg1", "arg2") stylespawn("binary", ["arg1", "arg2"]) or execFile(...)Better argument handling

Asynchronous vs synchronous versions

VersionBehaviorWhen to use
exec() / spawn() / execFile()Non-blockingMost applications
execSync() / spawnSync()Blocks until finishedSmall scripts or setup tasks

In most production Node.js apps, asynchronous versions are preferred because they do not block the event loop.

Take Quiz

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

  • 0 usually 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.

Take Quiz

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 prince binary 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
Take Quiz

Keep learning

Related questions

View more