Child Processes (exec & spawn)

Reviewed & published by Brayan K

The child_process module lets a Node program launch and control other programs — running shell commands, external tools, or another Node script — and read their output, errors, and exit codes.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll know when to reach for exec, execFile, spawn, or fork, how to capture streaming versus buffered output, how to pass arguments safely, and how to read stdout, stderr, and exit codes from the program you started.

What You'll Learn in This Lesson

1️⃣ exec & execFile — Buffered Output

exec runs a command through a shell and collects all of its output into memory, then calls your callback with (err, stdout, stderr). That's perfect for short commands with small output. Because it uses a shell, features like pipes and quotes work — but it also means untrusted input can be dangerous.

execFile is the safer cousin: it runs an executable directly and takes its arguments as an array, so there's no shell to misread special characters. Prefer it whenever any argument could come from user input.

const { exec } = require('node:child_process');

// exec runs a command in a SHELL and BUFFERS all output into memory.
// Great for short commands whose output is small.
exec('node -e "console.log(2 + 2)"', (err, stdout, stderr) => {
  if (err) {
    console.error('Command failed:', err.message);
    return;
  }
  console.log('stdout:', stdout.trim());
  console.log('stderr:', JSON.stringify(stderr));
});
const { execFile } = require('node:child_process');

// execFile runs a binary DIRECTLY (no shell) — faster and safer because
// arguments are passed as an array, not interpolated into a shell string.
execFile('node', ['-e', 'console.log("hi from execFile")'], (err, stdout) => {
  if (err) throw err;
  console.log(stdout.trim());
});

2️⃣ spawn — Streaming Output

When output is large or arrives over time, buffering it all in memory is wasteful and can hit a maxBuffer limit. spawn instead gives you streams: child.stdout and child.stderr emit 'data' events as chunks arrive, and a 'close' event fires with the exit code when the program ends.

Note that chunks can arrive in any grouping — three lines might come as one chunk or three — so we collect them and print once on 'close' to keep the output predictable.

const { spawn } = require('node:child_process');

// spawn STREAMS output as it arrives instead of buffering it all —
// perfect for large or long-running output.
const child = spawn('node', ['-e', 'for (let i = 1; i <= 3; i++) console.log("row " + i)']);

let collected = '';
child.stdout.on('data', (chunk) => {
  collected += chunk;          // chunks can arrive in any grouping
});

child.stderr.on('data', (chunk) => {
  process.stderr.write(`err: ${chunk}`);
});

child.on('close', (code) => {
  process.stdout.write(collected);
  console.log(`exit code: ${code}`);
});

3️⃣ fork — Node Talking to Node

fork is a special case of spawn built for launching another Node script. Beyond starting the process, it opens an automatic IPC channel, so the parent and child can pass structured messages with child.send(...) and process.on('message', ...).

This is how you offload work to a separate process while still coordinating. The child below sums an array it's sent, replies with the result, then exits. You need two files — the child script and the parent.

// child-worker.cjs — a separate Node program the parent forks.
process.on('message', (msg) => {
  const result = msg.numbers.reduce((a, b) => a + b, 0);
  process.send({ sum: result });
  process.exit(0);
});
const { fork } = require('node:child_process');

// fork needs the child to exist as a real file on disk — it is a separate
// program, not a function. The block above shows it; these three lines write
// it out so this example runs on its own. In your project you would just save
// it as child-worker.cjs next to this file.
require('node:fs').writeFileSync('child-worker.cjs',
  "process.on('message', (msg) => {\n" +
  "  const result = msg.numbers.reduce((a, b) => a + b, 0);\n" +
  "  process.send({ sum: result });\n" +
  "  process.exit(0);\n" +
  "});\n");

// fork spawns a NEW Node process AND opens an IPC channel automatically,
// so parent and child can exchange messages with send / on('message').
const child = fork('child-worker.cjs');

child.on('message', (msg) => {
  console.log('Child computed sum =', msg.sum);
});

child.on('exit', (code) => {
  console.log('Child exited with code', code);
});

// Send work to the child.
child.send({ numbers: [10, 20, 30] });

Your turn. The program below works once you fill in the two blanks marked ___. Follow the 👉 hints, then run it and compare with the expected output.

const { execFile } = require('node:child_process');

// 🎯 YOUR TURN — fill each ___ then run it.

// Run node with a tiny inline script that prints "answer is 42".
// Arguments go in an ARRAY (no shell quoting headaches!).
execFile(___, ['-e', 'console.log("answer is " + (6 * 7))'], (err, stdout, stderr) => {
  if (err) throw err;
  console.log(stdout.___());      // 👉 trim — drop the trailing newline
});
// First ___: the program to run, in 'single quotes' 👉 'node'

// ✅ Expected output:
//    answer is 42

Mini-Challenge: A Tiny Compute Server

No blanks this time — just a brief and an outline. Build the two files, run them, and check your output against the example in the comments. This ties together fork, send, and the message/exit events.

// 🎯 MINI-CHALLENGE: a tiny compute server using fork
//
// File 1 — square-child.cjs:
//   process.on('message', (n) => {
//     process.send(n * n);
//     process.exit(0);
//   });
//
// File 2 — main.cjs:
//   const { fork } = require('node:child_process');
//   const child = fork('square-child.cjs');
//   child.on('message', (sq) => console.log('square =', sq));
//   child.on('exit', (code) => console.log('done, code', code));
//   child.send(9);
//
// ✅ Example output:
//    square = 81
//    done, code 0

// write both files, then run: node main.cjs

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — child_process

FunctionBest for
exec(cmd, cb)short shell commands, buffered
execFile(file, args, cb)run a binary safely, no shell
spawn(cmd, args)large/streaming output
fork(modulePath)another Node script + IPC

🎉 Lesson Complete!

Practice quiz

How does exec deliver a command's output?

  • It streams output chunk by chunk
  • It writes directly to a file
  • It buffers all output in memory and hands it to a callback
  • It returns a Promise of a stream

Answer: It buffers all output in memory and hands it to a callback. exec runs the command in a shell and buffers stdout/stderr into memory, good for short commands with small output.

When should you prefer spawn over exec?

  • When output is large or long-running, so it can be streamed
  • When the command is a quick one-liner
  • When you need shell pipes
  • Never; exec is always better

Answer: When output is large or long-running, so it can be streamed. spawn streams output as events instead of buffering, avoiding 'maxBuffer exceeded' for large output.

What is the key difference between exec and execFile?

  • execFile always uses a shell
  • exec cannot capture output
  • execFile only runs Node scripts
  • execFile skips the shell and passes arguments as an array

Answer: execFile skips the shell and passes arguments as an array. execFile runs the binary directly with an args array, making it faster and safer against command injection.

Why is execFile safer than exec with untrusted input?

  • It encrypts the arguments
  • No shell means special characters cannot become commands
  • It runs as a different user
  • It validates the input automatically

Answer: No shell means special characters cannot become commands. Because execFile skips the shell, there is no shell to misinterpret special characters as command injection.

How does spawn deliver streaming output?

  • Through child.stdout and child.stderr 'data' events
  • Through a single return value
  • By writing to process.env
  • Only via the 'message' event

Answer: Through child.stdout and child.stderr 'data' events. spawn does not buffer; you read chunks via child.stdout.on('data', ...) and child.stderr.on('data', ...).

What does fork add on top of spawning a process?

  • Automatic shell access
  • A built-in HTTP server
  • An automatic IPC channel for message passing
  • Shared memory with the parent

Answer: An automatic IPC channel for message passing. fork launches another Node script and sets up an IPC channel, so parent and child exchange messages.

Which methods does fork use for parent-child messaging?

  • child.write(...) and process.read(...)
  • child.send(...) and process.on('message', ...)
  • exec(...) and eval(...)
  • child.emit(...) and process.listen(...)

Answer: child.send(...) and process.on('message', ...). The parent calls child.send(msg) and the child receives it with process.on('message', ...), and can reply with process.send.

When is fork the right choice over spawn?

  • When running git or ffmpeg
  • When you need a shell pipeline
  • When output must be buffered
  • When launching another Node.js script you want to message

Answer: When launching another Node.js script you want to message. fork is for launching another Node script with an IPC channel; use spawn or exec for non-Node programs.

What does an exit code of 0 from a child process mean?

  • A crash
  • Success
  • Still running
  • Permission denied

Answer: Success. An exit code of 0 means the child finished successfully; non-zero indicates an error.

Which event's callback gives you a child's numeric exit code?

  • 'data'
  • 'spawn'
  • 'exit' or 'close'
  • 'ready'

Answer: 'exit' or 'close'. Listen for the 'exit' or 'close' event whose callback receives the exit code.

Continue this course

Frequently asked questions

When should I use exec versus spawn?

Use exec for short commands whose output is small — it runs the command in a shell and buffers all stdout/stderr into memory, handing it to you in one callback. Use spawn when the output is large or streams over time (logs, file processing, long-running tools): spawn does not buffer, it streams chunks through child.stdout and child.stderr as events, so you can process data incrementally and avoid a 'maxBuffer exceeded' error. A handy rule: exec for quick one-liners, spawn for big or continuous output.

What is the difference between exec and execFile?

exec runs your command string through a shell (like /bin/sh or cmd.exe), so shell features like pipes, globbing, and variable expansion work — but that also means user input can become a command-injection risk. execFile skips the shell and runs the executable directly, passing arguments as a separate array. That makes execFile faster and much safer when any part of the command comes from untrusted input, because there is no shell to misinterpret special characters.

What is fork and how is it different from spawn?

fork is a specialized form of spawn for launching another Node.js script. On top of starting the process, it automatically sets up an IPC (inter-process communication) channel, so the parent and child can pass structured messages with child.send(...) and process.on('message', ...). Use fork when you want a separate Node process you can talk to — for example, offloading work to a worker process. Use spawn (or exec) when you're running a non-Node program like git, ffmpeg, or a shell command.

How do I read the exit code and errors from a child process?

Listen for the 'exit' or 'close' event, whose callback receives the numeric exit code (0 means success). With exec/execFile, a non-zero exit shows up as an Error in the callback, and you can read err.code for the status. With spawn, attach handlers to child.stdout and child.stderr for the data streams, and to the 'close' event for the final code. Always handle stderr and a non-zero exit — a child can fail without throwing in your parent process.