subprocess.on()

Listen for a child process's lifecycle events such as 'spawn', 'exit', 'close', 'error' and 'message'.

Signature
subprocess.on(eventName, listener)
  • Added in Node.js v0.1.101

Parameters

ParameterTypeDescription
$eventName<string>The event to listen for: 'spawn', 'error', 'exit', 'close', 'message' or 'disconnect'.
$listener<Function>The callback invoked with that event's arguments. 'exit' and 'close' receive (code, signal); 'error' receives (err); 'message' receives (message, sendHandle); 'spawn' and 'disconnect' receive nothing.

Return value

<ChildProcess>The same ChildProcess instance, so calls can be chained.

Errors

Emitting 'error' with no 'error' listener attached throws the error as an uncaught exception and crashes the parent process.

Changelog

  • PHP v15.1.0, v14.17.0The 'spawn' event was added, emitted once the child process has started successfully and before any other event.
  • PHP v2.2.0The ChildProcess class, which inherits on() from EventEmitter, is documented as a public class.

ChildProcess extends EventEmitter, so subprocess.on() is the standard listener registration. What matters is knowing which event actually answers your question.

Which event to listen for

EventFires when
'spawn'The process started successfully. Nothing else fires before it.
'error'It could not be spawned or killed, a send failed, or it was aborted.
'exit'The process ended. Its stdio streams may still have data.
'close'The process ended and all its stdio streams closed.
'message'An IPC message arrived (fork() children).

What trips people up

  • Reading collected output in 'exit' can truncate it. Use 'close', which is guaranteed to come after 'exit' and after the last 'data'.
  • An 'error' event with no listener is fatal, and a missing executable is exactly that case.
  • 'error' and 'exit' can both fire, so guard against running a completion handler twice.
  • A signalled child gives code === null with signal set; one of the two is always non-null.
  • 'spawn' only means the process started — with shell: true the shell can start fine and still fail to find your command.

Example

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

const child = spawn('node', ['-e', 'console.log("working"); process.exit(2)']);
const events = [];

child.on('spawn', () => events.push('spawn'));
child.stdout.on('data', (d) => events.push('stdout: ' + d.toString().trim()));
child.on('error', (err) => events.push('error ' + err.code));
child.on('exit', (code, signal) => events.push(`exit ${code} ${signal}`));
child.on('close', (code) => {
  events.push(`close ${code}`);
  console.log(events.join('\n'));
});

// spawn
// stdout: working
// exit 2 null
// close 2

When to use it

  • Resolve a promise once a build command has finished and all its output is collected.
  • Distinguish a missing binary from a command that ran and failed.
  • Receive results from a forked worker over IPC with the 'message' event.

Frequently asked questions

What is the difference between the close and exit events in Node.js child processes?

'exit' fires when the process itself ends, while 'close' fires later, once every stdio stream belonging to it has also closed. Read collected stdout in 'close', because in 'exit' the streams may still have buffered data.

How do I know when a child process is finished in Node.js?

Listen with subprocess.on('close', (code, signal) => ...), which is emitted after the process ended and its output streams closed. Also attach an 'error' listener, since a process that never started emits 'error' and never emits 'exit'.

Why does my Node app crash with an unhandled error when spawn fails?

Because ChildProcess is an EventEmitter, and an 'error' event with no listener is rethrown as an uncaught exception. Attach subprocess.on('error', handler) to every child, which is where ENOENT for a missing executable is delivered.

More examples

Wrapping a child process in a promise

The settled flag is the important part: a child that fails to spawn can emit 'error' and then 'close', which would otherwise call reject and resolve on the same promise. Resolving from 'close' rather than 'exit' is what guarantees the accumulated stdout is complete when the promise settles.

Example · javascript
import { spawn } from 'node:child_process';

function run(cmd, args) {
  return new Promise((resolve, reject) => {
    const child = spawn(cmd, args);
    let out = '';
    let settled = false;

    child.stdout.on('data', (d) => { out += d; });

    // 'error' and 'exit' can both fire, so settle exactly once.
    child.on('error', (err) => {
      if (settled) return;
      settled = true;
      reject(err);
    });

    // 'close' guarantees stdout has flushed; 'exit' does not.
    child.on('close', (code) => {
      if (settled) return;
      settled = true;
      code === 0 ? resolve(out.trim()) : reject(new Error('exited ' + code));
    });
  });
}

console.log(await run('node', ['-p', '1 + 1']));
await run('nope', []).catch((e) => console.log('rejected with', e.code));

// 2
// rejected with ENOENT

Telling a signal apart from an exit code

When a process is terminated by a signal its exit code is null, so a check like if (code !== 0) misses nothing but if (code) treats the kill as success. Sending the kill from the 'spawn' handler rather than immediately after spawn() avoids the race where the signal arrives before the process exists.

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

const child = spawn('node', ['-e', 'setInterval(() => {}, 1000)']);

child.on('exit', (code, signal) => {
  console.log('code:', code);
  console.log('signal:', signal);
  // One of the two is always non-null.
  console.log('ended by:', code === null ? 'signal' : 'exit code');
});

child.on('spawn', () => child.kill('SIGTERM'));

// code: null
// signal: SIGTERM
// ended by: signal

IPC messages from a forked child

'message' fires once per process.send() from the child, in order. 'disconnect' arrives when the IPC channel closes, and only then can the parent's event loop drain, since an open channel keeps it alive. The 'close' handler runs last, which is why it is the right place to report the full transcript.

Example · javascript
const { fork } = require('node:child_process');

const child = fork('./tally-worker.js');
const seen = [];

child.on('message', (msg) => seen.push(msg.value));
child.on('disconnect', () => seen.push('disconnected'));
child.on('close', (code) => {
  console.log(seen);
  console.log('exit code:', code);
});

child.send({ start: 3 });

// tally-worker.js
// process.on('message', ({ start }) => {
//   for (let i = 1; i <= start; i++) process.send({ value: i });
//   process.disconnect();
// });

// [ 1, 2, 3, 'disconnected' ]
// exit code: 0

Discussion

  • Be the first to comment on this lesson.