Back to Blog
Node.jsChild ProcessConcurrencySystem

Node.js Child Process Guide

Master child processes in Node.js. From spawn to exec to fork patterns for parallel execution.

B
Bootspring Team
Engineering
February 5, 2021
8 min read

Child processes enable parallel execution and system command integration. Here's how to use them effectively.

spawn vs exec vs fork#

1const { spawn, exec, execFile, fork } = require('child_process'); 2 3// spawn - streaming output, best for long-running processes 4const ls = spawn('ls', ['-la', '/tmp']); 5 6ls.stdout.on('data', (data) => { 7 console.log(`stdout: ${data}`); 8}); 9 10ls.stderr.on('data', (data) => { 11 console.error(`stderr: ${data}`); 12}); 13 14ls.on('close', (code) => { 15 console.log(`child process exited with code ${code}`); 16}); 17 18// exec - buffered output, runs in shell 19exec('ls -la /tmp', (error, stdout, stderr) => { 20 if (error) { 21 console.error(`exec error: ${error}`); 22 return; 23 } 24 console.log(`stdout: ${stdout}`); 25 console.error(`stderr: ${stderr}`); 26}); 27 28// execFile - like exec but doesn't spawn shell 29execFile('node', ['--version'], (error, stdout) => { 30 if (error) throw error; 31 console.log(`Node version: ${stdout}`); 32}); 33 34// fork - special spawn for Node.js scripts with IPC 35const child = fork('./worker.js'); 36child.send({ type: 'start', data: [1, 2, 3] }); 37child.on('message', (msg) => { 38 console.log('Message from child:', msg); 39});

Promisified Execution#

1const { exec, spawn } = require('child_process'); 2const { promisify } = require('util'); 3 4const execPromise = promisify(exec); 5 6async function runCommand(cmd) { 7 try { 8 const { stdout, stderr } = await execPromise(cmd); 9 return { stdout, stderr }; 10 } catch (error) { 11 throw new Error(`Command failed: ${error.message}`); 12 } 13} 14 15// Usage 16async function main() { 17 const result = await runCommand('ls -la'); 18 console.log(result.stdout); 19} 20 21// Spawn with promise 22function spawnPromise(command, args, options = {}) { 23 return new Promise((resolve, reject) => { 24 const child = spawn(command, args, options); 25 let stdout = ''; 26 let stderr = ''; 27 28 child.stdout?.on('data', (data) => { 29 stdout += data; 30 }); 31 32 child.stderr?.on('data', (data) => { 33 stderr += data; 34 }); 35 36 child.on('close', (code) => { 37 if (code === 0) { 38 resolve({ stdout, stderr, code }); 39 } else { 40 reject(new Error(`Process exited with code ${code}: ${stderr}`)); 41 } 42 }); 43 44 child.on('error', reject); 45 }); 46}

Streaming Data#

1const { spawn } = require('child_process'); 2const fs = require('fs'); 3 4// Pipe between processes 5const grep = spawn('grep', ['error']); 6const cat = spawn('cat', ['logs.txt']); 7 8cat.stdout.pipe(grep.stdin); 9 10grep.stdout.on('data', (data) => { 11 console.log(`Found: ${data}`); 12}); 13 14// Write to stdin 15const sort = spawn('sort'); 16 17sort.stdout.on('data', (data) => { 18 console.log(`Sorted:\n${data}`); 19}); 20 21sort.stdin.write('banana\n'); 22sort.stdin.write('apple\n'); 23sort.stdin.write('cherry\n'); 24sort.stdin.end(); 25 26// Stream to file 27const ls = spawn('ls', ['-la']); 28const output = fs.createWriteStream('listing.txt'); 29 30ls.stdout.pipe(output); 31 32ls.on('close', (code) => { 33 console.log(`Listing saved, exit code: ${code}`); 34});

IPC Communication (fork)#

1// parent.js 2const { fork } = require('child_process'); 3 4const workers = []; 5const numWorkers = 4; 6 7// Create worker pool 8for (let i = 0; i < numWorkers; i++) { 9 const worker = fork('./worker.js'); 10 11 worker.on('message', (msg) => { 12 console.log(`Worker ${i} completed:`, msg); 13 }); 14 15 worker.on('exit', (code) => { 16 console.log(`Worker ${i} exited with code ${code}`); 17 }); 18 19 workers.push(worker); 20} 21 22// Distribute work 23const tasks = [1, 2, 3, 4, 5, 6, 7, 8]; 24tasks.forEach((task, index) => { 25 const worker = workers[index % numWorkers]; 26 worker.send({ type: 'task', data: task }); 27}); 28 29// Shutdown workers 30setTimeout(() => { 31 workers.forEach(worker => { 32 worker.send({ type: 'shutdown' }); 33 }); 34}, 5000); 35 36// worker.js 37process.on('message', (msg) => { 38 switch (msg.type) { 39 case 'task': 40 const result = msg.data * 2; // Do work 41 process.send({ type: 'result', data: result }); 42 break; 43 case 'shutdown': 44 process.exit(0); 45 break; 46 } 47});

Detached Processes#

1const { spawn } = require('child_process'); 2const fs = require('fs'); 3 4// Run process that survives parent exit 5const out = fs.openSync('./daemon.log', 'a'); 6const err = fs.openSync('./daemon.log', 'a'); 7 8const daemon = spawn('node', ['daemon.js'], { 9 detached: true, 10 stdio: ['ignore', out, err], 11}); 12 13// Allow parent to exit independently 14daemon.unref(); 15 16console.log(`Daemon started with PID: ${daemon.pid}`); 17process.exit(0); 18 19// daemon.js 20setInterval(() => { 21 console.log(`Daemon running at ${new Date().toISOString()}`); 22}, 1000);

Shell Commands#

1const { exec, spawn } = require('child_process'); 2 3// Complex shell commands with exec 4exec('cat *.txt | grep error | wc -l', { shell: '/bin/bash' }, (error, stdout) => { 5 console.log(`Error count: ${stdout.trim()}`); 6}); 7 8// Shell with spawn 9const shell = spawn('bash', ['-c', 'for i in 1 2 3; do echo $i; sleep 1; done']); 10 11shell.stdout.on('data', (data) => { 12 console.log(data.toString()); 13}); 14 15// Environment variables 16exec('echo $MY_VAR', { 17 env: { ...process.env, MY_VAR: 'custom_value' } 18}, (error, stdout) => { 19 console.log(stdout); // custom_value 20}); 21 22// Working directory 23exec('pwd', { cwd: '/tmp' }, (error, stdout) => { 24 console.log(stdout); // /tmp 25});

Timeout and Kill#

1const { exec, spawn } = require('child_process'); 2 3// Timeout with exec 4exec('sleep 10', { timeout: 2000 }, (error, stdout, stderr) => { 5 if (error) { 6 console.log('Process timed out'); 7 } 8}); 9 10// Manual timeout with spawn 11const longProcess = spawn('sleep', ['10']); 12 13const timeout = setTimeout(() => { 14 longProcess.kill('SIGTERM'); 15 console.log('Process killed due to timeout'); 16}, 2000); 17 18longProcess.on('close', () => { 19 clearTimeout(timeout); 20}); 21 22// Kill signals 23const proc = spawn('node', ['server.js']); 24 25// Graceful shutdown 26proc.kill('SIGTERM'); 27 28// Force kill 29setTimeout(() => { 30 if (!proc.killed) { 31 proc.kill('SIGKILL'); 32 } 33}, 5000); 34 35// Handle in child process 36process.on('SIGTERM', () => { 37 console.log('Received SIGTERM, cleaning up...'); 38 // Cleanup 39 process.exit(0); 40});

Worker Pool Pattern#

1const { fork } = require('child_process'); 2const os = require('os'); 3 4class WorkerPool { 5 constructor(workerScript, poolSize = os.cpus().length) { 6 this.workerScript = workerScript; 7 this.poolSize = poolSize; 8 this.workers = []; 9 this.freeWorkers = []; 10 this.taskQueue = []; 11 12 this.init(); 13 } 14 15 init() { 16 for (let i = 0; i < this.poolSize; i++) { 17 this.createWorker(); 18 } 19 } 20 21 createWorker() { 22 const worker = fork(this.workerScript); 23 24 worker.on('message', (result) => { 25 worker.currentTask?.resolve(result); 26 worker.currentTask = null; 27 this.freeWorkers.push(worker); 28 this.processQueue(); 29 }); 30 31 worker.on('error', (error) => { 32 worker.currentTask?.reject(error); 33 this.workers = this.workers.filter(w => w !== worker); 34 this.createWorker(); 35 }); 36 37 this.workers.push(worker); 38 this.freeWorkers.push(worker); 39 } 40 41 processQueue() { 42 if (this.taskQueue.length === 0 || this.freeWorkers.length === 0) { 43 return; 44 } 45 46 const worker = this.freeWorkers.shift(); 47 const task = this.taskQueue.shift(); 48 49 worker.currentTask = task; 50 worker.send(task.data); 51 } 52 53 execute(data) { 54 return new Promise((resolve, reject) => { 55 this.taskQueue.push({ data, resolve, reject }); 56 this.processQueue(); 57 }); 58 } 59 60 async shutdown() { 61 for (const worker of this.workers) { 62 worker.kill(); 63 } 64 } 65} 66 67// Usage 68const pool = new WorkerPool('./compute-worker.js', 4); 69 70async function main() { 71 const results = await Promise.all([ 72 pool.execute({ task: 'fibonacci', n: 40 }), 73 pool.execute({ task: 'factorial', n: 100 }), 74 pool.execute({ task: 'prime', n: 10000 }), 75 ]); 76 77 console.log('Results:', results); 78 await pool.shutdown(); 79}

Stdio Configuration#

1const { spawn } = require('child_process'); 2 3// Inherit stdio (child uses parent's console) 4const interactive = spawn('vim', ['file.txt'], { 5 stdio: 'inherit', 6}); 7 8// Pipe specific streams 9const mixed = spawn('command', [], { 10 stdio: ['pipe', 'pipe', 'inherit'], // stdin, stdout piped; stderr inherited 11}); 12 13// Ignore streams 14const silent = spawn('command', [], { 15 stdio: 'ignore', 16}); 17 18// Custom file descriptors 19const fs = require('fs'); 20const out = fs.openSync('./out.log', 'w'); 21const err = fs.openSync('./err.log', 'w'); 22 23const logged = spawn('command', [], { 24 stdio: ['ignore', out, err], 25}); 26 27// IPC channel 28const withIPC = spawn('node', ['child.js'], { 29 stdio: ['pipe', 'pipe', 'pipe', 'ipc'], 30}); 31 32withIPC.on('message', (msg) => { 33 console.log('Message via IPC:', msg); 34}); 35 36withIPC.send({ hello: 'world' });

Error Handling#

1const { spawn, exec } = require('child_process'); 2 3// Handle spawn errors 4const child = spawn('nonexistent-command'); 5 6child.on('error', (error) => { 7 console.error('Failed to start subprocess:', error.message); 8}); 9 10// Handle exit codes 11const failing = spawn('node', ['-e', 'process.exit(1)']); 12 13failing.on('close', (code, signal) => { 14 if (code !== 0) { 15 console.error(`Process failed with code ${code}`); 16 } 17 if (signal) { 18 console.error(`Process killed with signal ${signal}`); 19 } 20}); 21 22// Comprehensive error handling 23async function safeExec(command, options = {}) { 24 return new Promise((resolve, reject) => { 25 const child = exec(command, { 26 timeout: 30000, 27 maxBuffer: 1024 * 1024, 28 ...options, 29 }, (error, stdout, stderr) => { 30 if (error) { 31 error.stdout = stdout; 32 error.stderr = stderr; 33 reject(error); 34 } else { 35 resolve({ stdout, stderr }); 36 } 37 }); 38 39 child.on('error', reject); 40 }); 41} 42 43// Usage 44try { 45 const result = await safeExec('risky-command'); 46 console.log(result.stdout); 47} catch (error) { 48 console.error('Command failed:', error.message); 49 console.error('Stderr:', error.stderr); 50}

Cross-Platform Commands#

1const { spawn } = require('child_process'); 2const os = require('os'); 3 4function crossSpawn(command, args = [], options = {}) { 5 const isWindows = os.platform() === 'win32'; 6 7 if (isWindows) { 8 return spawn('cmd.exe', ['/c', command, ...args], options); 9 } 10 11 return spawn(command, args, options); 12} 13 14// Or use shell option 15const child = spawn('echo', ['hello'], { 16 shell: true, // Uses cmd.exe on Windows, /bin/sh on Unix 17}); 18 19// Find executable 20function which(command) { 21 const isWindows = os.platform() === 'win32'; 22 const cmd = isWindows ? 'where' : 'which'; 23 24 return new Promise((resolve, reject) => { 25 exec(`${cmd} ${command}`, (error, stdout) => { 26 if (error) reject(error); 27 else resolve(stdout.trim().split('\n')[0]); 28 }); 29 }); 30}

Best Practices#

Process Management: ✓ Always handle 'error' events ✓ Clean up child processes on exit ✓ Set appropriate timeouts ✓ Handle both stdout and stderr Performance: ✓ Use spawn for streaming data ✓ Use fork for CPU-intensive tasks ✓ Implement worker pools for parallelism ✓ Reuse processes when possible Security: ✓ Avoid shell: true with user input ✓ Sanitize command arguments ✓ Use execFile over exec when possible ✓ Limit resource usage Error Handling: ✓ Check exit codes ✓ Handle signals properly ✓ Implement graceful shutdown ✓ Log stderr output

Conclusion#

Child processes enable parallel execution and system integration. Use spawn for streaming, exec for simple commands, fork for Node.js workers, and implement proper error handling. Worker pools maximize CPU utilization for heavy computations.

Share this article

Help spread the word about Bootspring