Page Content

Tutorials

How does Node.js handle child processes? Explained With Code

Node.js Child Processes

The child_process module, a core Node.js module, spawns child processes to execute shell commands, run executable files, and start other programs. This lets multiprocessing and application speed improve by handling long-running or CPU-intensive tasks without stopping Node.js’s main thread. The kernel handles such work, allowing Node.js to focus on other tasks.

Here are crucial child_process module methods:

Executing Commands

The exec() function runs shell commands and stores their output. It starts a shell process and runs the command.

Syntax and Parameters: child_process.exec(command[, options][, callback])

  • command: A required string containing the shell command to be executed, which can include shell piping or redirection.
  • options: An optional object that allows customisation of the execution. Key options include:
    • encoding: Default is 'utf8'.
    • timeout: Maximum milliseconds to wait before killing the process.
    • maxBuffer: Maximum amount of data (in bytes) allowed on stdout or stderr before the process is killed (default 200KB).
    • killSignal: The signal sent to the child process after a timeout (default 'SIGTERM').
    • cwd: Sets the current working directory for the child process.
    • env: An object of key-value pairs for environment variables.
    • shell: The shell to execute the command with (default /bin/sh on UNIX, cmd.exe on Windows).
    • uid and gid: User and group identity for the process, in terms of standard system permissions.
  • callback: An optional function that is invoked when the command finishes. It receives three arguments:
    • err: An Error object if the command failed to run.
    • stdout: A Buffer object (or string, depending on encoding) containing the command’s standard output.
    • stderr: A Buffer object (or string) containing the command’s standard error output.

Synchronous Version: Synchronous execution is possible with execSync(). If a callback is received, it blocks the main thread and throws an error; otherwise, it returns the standard output immediately.

Example: Listing files in a directory

const { exec } = require('child_process');
exec('ls -lh', (error, stdout, stderr) => {
  if (error) {
    console.error(`error: ${error.message}`);
    return;
  }
  if (stderr) {
    console.error(`stderr: ${stderr}`);
    return;
  }
  console.log(`stdout:\n${stdout}`);
});

Output:

stdout:
total 4.0K
-rw-rw-r-- 1 sammy sammy 280 Jul 27 16:35 listFiles.js

This output lists the contents of the directory in a long format.

Running Executable Files

To launch an executable file directly, use the execFile() function. In contrast to exec(), it usually doesn’t create a shell, which makes it marginally more efficient. When certain executables need to be run without the use of shell features like piping or redirection, this function is perfect.

Syntax and Parameters: child_process.execFile(file[, args][, options][, callback])

  • file: Where the executable file is located.
  • args: An optional array of arguments to provide to the executable is called args. A significant distinction from exec(), where parameters are a part of the command string, is this.
  • callback and options: Very similar to those for exec().

Synchronous Version: The synchronous version is execFileSync().

Note on Windows Scripts: execFile() does not construct a shell, hence it cannot execute batch (.bat) or command (.cmd) files on Windows. exec() or spawn() should be used for these.

Example: Running a bash script to download and Base64 encode an image

const { execFile } = require('child_process');
const path = require('path');
execFile(path.join(__dirname, '/processNodejsImage.sh'), (error, stdout, stderr) => {
  if (error) {
    console.error(`error: ${error.message}`);
    return;
  }
  if (stderr) {
    console.error(`stderr: ${stderr}`);
    return;
  }
  console.log(`stdout:\n${stdout}`);
});

Output (simplified for brevity):

stdout:
... (Base64 encoded image data) ...

The shell script’s successful execution as a child process is demonstrated by this.

Spawning Processes

Using a command and array of arguments, spawn() begins a new process and streams unbuffered output. This is ideal for commands that produce a lot of data or run for a long time because data is processed in chunks rather than held in memory.

Syntax and Parameters: child_process.spawn(command[, args][, options])

  1. command: The command that has to be carried out.
  2. args: The command’s array of parameters.
  3. options: An optional object for configuration. It also supports cwd, env, uid, gid options similar to exec(). A notable option is stdio, which defines the child’s input/output streams.

Interacting with Streams: The spawn() method yields an instance of ChildProcess, which exposes stream-based stdout and stderr stream.Readable examples. These streams allow you to manage the output as it becomes available by attaching event listeners. Frequent occurrences include:

  1. ‘data’: Released within the stream when data is accessible.
  2. ‘error’: Issued in the event that the command is interrupted or does not execute.
  3. ‘close’: The 'close' command is released once the child process is finished.

Synchronous Version: The synchronous version is spawnSync().

Example: Finding files in the current directory

const { spawn } = require('child_process');
const child = spawn('find', ['.']);
child.stdout.on('data', (data) => {
  console.log(`stdout:\n${data}`);
});
child.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});
child.on('error', (error) => {
  console.error(`error: ${error.message}`);
});
child.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

Output:

stdout:
.
./listFiles.js
./getNodejsImage.js
./processNodejsImage.sh
./nodejs-logo.svg
child process exited with code 0

This output shows find command listing files and folders, with messages logged from both stdout and close events.

Forking Node.js Processes

A specialised version of spawn(), fork() is intended just for spawning new Node.js processes. The built-in communication channel that allows bidirectional messaging between the parent and child processes is its main benefit. Coordinated execution is made possible by this, as the parent can communicate with the child and the youngster can communicate with the parent.

Syntax and Parameters: child_process.fork(modulePath[, args][, options])

  • modulePath: The path to the Node.js program to be executed in the child process.
  • args: An optional array of arguments, accessible via process.argv in the child process.
  • options: An optional object, including cwd, env, encoding, execPath, and notably, silent. Setting silent: true disables the child’s stdio from being associated with the parent’s.

Communication: The global process object is used for communication between the parent and the kid.

  • Parent to Child: child.send(message).
  • Child to Parent: process.send(message).
  • On('message', callback) allows both parent and child to listen for 'message' events.

Important Note: A forked child process must explicitly execute process.exit() when its job is finished; it does not automatically terminate when it is finished.

Example: Offloading a slow, blocking function to a child process

getCount.js (Child Process):

const slowFunction = () => {
  let counter = 0;
  while (counter < 5000000000) { // Simulates a long-running task
    counter++;
  }
  return counter;
};
process.on('message', (message) => {
  if (message === 'START') {
    console.log('Child process received START message');
    let slowResult = slowFunction();
    let responseMessage = `{"totalCount":${slowResult}}`;
    process.send(responseMessage); // Send result back to parent
  }
});

httpServer.js (Parent Process):

const http = require('http');
const { fork } = require('child_process');
const host = 'localhost';
const port = 8000;
const requestListener = function (req, res) {
  if (req.url === '/total') {
    const child = fork(__dirname + '/getCount'); // Spawn child process
    child.on('message', (message) => {
      console.log('Returning /total results');
      res.setHeader('Content-Type', 'application/json');
      res.writeHead(200);
      res.end(message);
    });
    child.send('START'); // Send message to child to start computation
  } else if (req.url === '/hello') {
    console.log('Returning /hello results');
    res.setHeader('Content-Type', 'application/json');
    res.writeHead(200);
    res.end(`{"message":"hello"}`);
  }
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
  console.log(`Server is running on http://${host}:${port}`);
});

The /hello response is not blocked by the slowFunction executing in the child process, which logs its message independently, when you execute httpServer.js and hit the /total and /hello destinations simultaneously. This illustrates how fork() makes CPU-bound processes concurrent.

Output from httpServer.js terminal:

Server is running on http://localhost:8000
Child process received START message
Returning /hello results
Returning /total results

(The crucial point is that /hello does not wait for /total to complete its laborious computation; the precise sequence of “Returning /hello results” and “Child process received START message” may differ slightly due to their asynchronous nature.)

Index