Run other programs from Node.js with spawn, exec, execFile and fork. Stream output, handle exit codes and timeouts, and avoid shell injection.

Bishal Biswas
Self Learner
What is a child process?
A process is a running program. When you type node app.js, the operating system starts one process for it. Each process has its own memory and its own ID number, called a PID.
A child process is a program that your Node program starts. Your program is the parent.
Think of a head chef who hands jobs to other cooks. The head chef does not chop every onion. They say “you, peel these”, carry on with their own work, and check back when the cook shouts “done”.
You would use child processes to:
- Run another tool, like
git,ffmpegor a Python script. - Run a slow, CPU-heavy job without freezing your main program.
- Start a helper Node script that talks back to you.
Everything lives in the built-in node:child_process module. It has four main functions:
| Function | Best for | Output arrives |
|---|---|---|
execFile |
Running a program and collecting its output | All at once, at the end |
exec |
Running a shell command string | All at once, at the end |
spawn |
Long-running programs or lots of output | As a stream, while it runs |
fork |
Running another Node script that sends messages back | As messages |
Note: The examples spawn
process.execPath, which is the full path to the Node program running your script. Every reader has Node, so these examples work the same on Windows, macOS and Linux.
execFile: run a program, get the output
execFile runs a program with a list of arguments and hands you everything it printed once it finishes. It uses a callback, so wrap it with promisify to get a promise you can await:
// version.mjs
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const { stdout, stderr } = await execFileAsync(process.execPath, ['--version']);
console.log('Node version:', stdout.trim());
Node version: v24.12.0
stdoutis standard output, what the program printed normally.stderris standard error, where programs print warnings and errors.
Each argument is a separate string in the array. Node passes them straight to the program, so spaces and quotes inside an argument are safe.
When the child fails
A program reports success or failure with an exit code: 0 means success, anything else means failure. A non-zero exit code makes the promise reject:
try {
await execFileAsync(process.execPath, ['-e', 'process.exit(3)']);
} catch (err) {
console.log('failed with code', err.code); // prints: failed with code 3
}
If the program does not exist at all, err.code is the string 'ENOENT' (“error, no entry”) instead of a number.
Timeouts
Pass timeout in milliseconds and Node kills a child that runs too long:
try {
await execFileAsync(process.execPath, ['-e', 'setTimeout(() => {}, 5000)'], { timeout: 500 });
} catch (err) {
console.log('timed out:', err.killed, err.signal); // prints: timed out: true SIGTERM
}
SIGTERM is the polite “please stop” signal, a message the operating system sends to a process.
exec: run a shell command
exec takes one string and runs it through a shell: cmd.exe on Windows, /bin/sh elsewhere. The shell is what lets you use things like pipes (|) and wildcards (*).
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
const { stdout } = await execAsync('node -p "1 + 2"');
console.log(stdout.trim()); // prints: 3
It is convenient for one-off commands you write yourself. Shell syntax differs between Windows and Unix, though, so prefer execFile when you can.
Note: Never build an
execstring from user input. The Common mistakes section below shows why.
spawn: stream the output as it happens
execFile and exec keep all output in memory until the child ends. That is a problem for a program that runs for an hour, or prints gigabytes.
spawn starts the child and gives you its output as streams (you met these in the Streams Fundamentals lesson):
// spawn.mjs
import { spawn } from 'node:child_process';
const child = spawn(process.execPath, [
'-e',
'console.log("Hello from the child"); console.error("a warning")',
]);
child.stdout.on('data', (chunk) => process.stdout.write(`[out] ${chunk}`));
child.stderr.on('data', (chunk) => process.stdout.write(`[err] ${chunk}`));
child.on('close', (code) => console.log('child exited with code', code));
console.log('child started with pid', child.pid);
child started with pid 9544
[out] Hello from the child
[err] a warning
child exited with code 0
Notice “child started” prints first. spawn returns immediately and the parent keeps working while the child runs.
Useful events on the child:
'close': the child exited and its output streams are finished. Use this one when you read output.'exit': the child process ended.'error': the child could not be started (for example, the program was not found).
Waiting with await
once from node:events turns an event into a promise:
import { once } from 'node:events';
const [code] = await once(child, 'close');
console.log('finished with', code);
Sending input
The child’s stdin (standard input) is a writable stream. Whatever you write, the child reads:
const upper = spawn(process.execPath, [
'-e',
'process.stdin.on("data", (d) => process.stdout.write(String(d).toUpperCase()))',
]);
upper.stdout.on('data', (d) => console.log(String(d).trim())); // prints: HELLO ASHA
upper.stdin.write('hello asha\n');
upper.stdin.end(); // tell the child there is no more input
Sharing your terminal
If you just want the child to print straight to your terminal, use stdio: 'inherit':
spawn(process.execPath, ['-e', 'console.log("straight to your terminal")'], { stdio: 'inherit' });
Stopping a child
Call child.kill() to send SIGTERM, or pass an AbortSignal (see the Promise Combinators and AbortSignal lesson):
const controller = new AbortController();
const looper = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
signal: controller.signal,
});
looper.on('error', (err) => console.log(err.name)); // prints: AbortError
setTimeout(() => controller.abort(), 200);
fork: Node talking to Node
fork is a special spawn for Node scripts. It sets up an IPC channel (inter-process communication), a private line the parent and child use to send each other JavaScript objects.
The child:
// worker.mjs
process.on('message', (msg) => {
const total = msg.numbers.reduce((a, b) => a + b, 0);
process.send({ total });
process.disconnect(); // close the channel so the child can exit
});
The parent:
// main.mjs
import { fork } from 'node:child_process';
const child = fork(new URL('./worker.mjs', import.meta.url));
child.on('message', (msg) => console.log('parent got:', msg));
child.on('exit', (code) => console.log('child exited with code', code));
child.send({ numbers: [4, 8, 15] });
parent got: { total: 27 }
child exited with code 0
child.send()andprocess.send()send a message. Objects are copied, not shared.'message'events receive them on the other side.process.sendonly exists in a child started withfork, soif (process.send)tells a script whether it was forked.
The child is a whole separate Node process, so heavy work there never blocks the parent. The cost is start-up time and memory: each one is a full copy of Node. For CPU work inside one process, the Worker Threads lesson shows a lighter option.
Sync versions
Each function has a blocking twin: execFileSync, execSync and spawnSync. They freeze your program until the child finishes, which is fine in a build script or CLI tool but bad in a server:
import { execFileSync } from 'node:child_process';
const platform = execFileSync(process.execPath, ['-p', 'process.platform'], { encoding: 'utf8' });
console.log(platform.trim()); // prints: win32 (or linux, or darwin)
encoding: 'utf8' gives you a string instead of a Buffer.
Common mistakes
Shell injection
// DANGER: the user controls part of a shell command
const file = req.query.file; // imagine: "notes.txt & del /q *"
await execAsync(`type ${file}`);
The shell treats &, ; and | as “run another command”. A malicious value runs whatever it likes on your machine. This is called shell injection. Use execFile or spawn with an argument array, which never goes through a shell:
await execFileAsync(process.execPath, ['print-file.mjs', file]); // file is just one argument
Running npm and other .cmd files on Windows
spawn('npm', ['--version']); // works on macOS/Linux, error ENOENT on Windows
On Windows, npm is really npm.cmd, a batch file. Node will not run .cmd or .bat files without a shell (calling spawn('npm.cmd', ...) throws EINVAL). Use the shell, and pass the whole command as one string:
spawn('npm --version', { shell: true, stdio: 'inherit' });
Passing an argument array together with shell: true prints a DEP0190 deprecation warning in Node 24, because the arguments are glued together without escaping. Only do this with fixed strings you wrote, never user input.
Forgetting to read the output
If a spawned child prints a lot and nobody reads stdout, the pipe fills up and the child can stall. Either listen for 'data', pipe it somewhere, or use stdio: 'inherit' or 'ignore'.
Not handling ‘error’
spawn('no-such-program'); // crashes the parent with an unhandled 'error' event
Always add child.on('error', ...) when the program might not exist.
Quick recap
node:child_processlets your program start other programs.execFileruns a program with an argument array and returns all output at the end. Promisify it forawait.execruns a shell command string. Never put user input in it.spawnstreamsstdoutandstderrwhile the child runs. Use it for long jobs and big output.forkstarts a Node script with an IPC channel forsend()and'message'.- A non-zero exit code means failure.
timeoutandAbortSignalstop runaway children. - Spawning
process.execPathis a handy cross-platform way to run Node itself.
// processes.mjs: run with node processes.mjs (works on Windows, macOS and Linux)
import { spawn, execFile, fork } from 'node:child_process';
import { promisify } from 'node:util';
import { once } from 'node:events';
import { fileURLToPath } from 'node:url';
// fork() runs THIS file again as a child. process.send only exists in a forked child.
if (process.send) {
process.on('message', ({ numbers }) => {
const total = numbers.reduce((a, b) => a + b, 0);
process.send({ total, pid: process.pid });
process.disconnect(); // close the channel so the child can exit
});
} else {
// --- 1. execFile: run a program, get all output at the end ---
const execFileAsync = promisify(execFile);
const { stdout } = await execFileAsync(process.execPath, ['--version']);
console.log('1. Node version from a child:', stdout.trim());
// --- 2. spawn: stream output while the child runs ---
const counter = spawn(process.execPath, [
'-e',
'for (let i = 1; i <= 3; i++) console.log("tick " + i); console.error("done counting")',
]);
// A chunk can hold several lines, so split it before printing
const show = (label) => (chunk) =>
String(chunk).trim().split('\n').forEach((line) => console.log(`2. [${label}] ${line.trim()}`));
counter.stdout.on('data', show('out'));
counter.stderr.on('data', show('err'));
const [code] = await once(counter, 'close');
console.log('2. spawn finished with exit code', code);
// --- 3. Failures: a non-zero exit code rejects the promise ---
try {
await execFileAsync(process.execPath, ['-e', 'process.exit(3)']);
} catch (err) {
console.log('3. child failed with code', err.code);
}
// --- 4. Timeouts: kill a child that takes too long ---
try {
await execFileAsync(process.execPath, ['-e', 'setTimeout(() => {}, 10000)'], { timeout: 300 });
} catch (err) {
console.log('4. killed after timeout:', err.killed, err.signal);
}
// --- 5. fork: a Node child with a message channel ---
const child = fork(fileURLToPath(import.meta.url));
child.send({ numbers: [4, 8, 15, 16, 23, 42] });
const [reply] = await once(child, 'message');
console.log(`5. child ${reply.pid} says the total is ${reply.total}`);
await once(child, 'exit');
console.log('All children finished. Parent pid:', process.pid);
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready