Running other programs: child_process

Node.js · Advanced · 14 min

Build tools, CLIs and backends constantly shell out to git, ffmpeg, image tools or other Node scripts.

node:child_process starts other programs. Pick the function by what you need:

  • `execFile(file, args)` — run a program and buffer its output; great for short commands like git rev-parse HEAD. Output beyond maxBuffer (1 MB) is an error.
  • `spawn(cmd, args)` — returns a ChildProcess with stdout/stderr/stdin streams; use it for long-running or chatty processes and large output. stdio: "inherit" connects the child directly to your terminal.
  • `exec(commandString)` — runs a string through a shell (/bin/sh or cmd.exe). Convenient for pipes and globs, dangerous with user input.
  • `fork(modulePath)` — spawns a new Node process with an IPC channel, so parent and child exchange objects with send() / on("message").

The security rule: never build a shell command from untrusted input. exec(\convert ${filename} out.png\) with a filename like x; rm -rf ~ runs both commands. execFile and spawn without shell: true pass arguments directly to the program, so no shell parsing happens.

Exit status arrives on the 'close' event (after stdio streams finish) as code, or signal if killed. A non-zero code is not an exception with spawn — check it yourself. The promisified execFile rejects on non-zero exit, with stdout/stderr attached to the error.

Control: child.kill("SIGTERM"), the timeout and signal (AbortSignal) options, cwd and env for the child's environment. The *Sync variants block the event loop; fine in build scripts, not in servers.

Each child is a full OS process — tens of milliseconds to start, separate memory. For heavy JavaScript work prefer worker threads; use child processes for other programs or isolation.

Example

import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
import { once } from "node:events";

const run = promisify(execFile);

// Short command, arguments passed safely (no shell)
const { stdout } = await run("node", ["-p", "process.versions.v8"]);
console.log("v8", stdout.trim());

// Long-running command with streamed output
const child = spawn("node", ["-e", "for (let i = 1; i <= 3; i++) console.log('line', i)"], {
  signal: AbortSignal.timeout(5000),
});
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => process.stdout.write(`[child] ${chunk}`));
child.on("error", (err) => console.error("failed to start or aborted:", err.message));

const [code] = await once(child, "close");
console.log("exit code", code);       // 0

// DON'T: exec(`grep ${userInput} file.txt`)  — shell injection
// DO:    execFile("grep", [userInput, "file.txt"])

Remember

  • execFile for short commands, spawn for streaming, fork for Node-to-Node IPC
  • Never interpolate user input into exec or shell: true commands
  • Check the exit code; handle the 'error' event

Watch out for

  • Using exec with string interpolation of user-controlled values.
  • Buffering huge output with exec/execFile and hitting maxBuffer.

Check yourself

  1. You need to run ffmpeg on a user-uploaded filename. Which call is safe?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nodejs.org/docs/latest-v22.x/api/child_process.html