Skip to content
JavaScript Basics: Drain Child Output Before Reporting Completion

JavaScript Basics: Drain Child Output Before Reporting Completion

October 30, 2026

Use spawn when you want to consume child output as it arrives instead of waiting for a buffered execFile result. Both stdout and stderr need consumers, and the child’s exit status still needs checking. This fixture launches the same Node executable with a fixed script and no shell.

Prepare a scratch directory

Tested on Linux with Node.js v24.19.0. You need Node, Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. No npm packages or remote services are needed. The .mjs extension selects ES modules.

Keep using the same terminal. Stop if setup fails, before saving the source:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Run the example

Save this as stream-child.mjs:

import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { once } from 'node:events';

const child = spawn(process.execPath, ['-e', String.raw`
  process.stdout.write('alpha\n');
  process.stdout.write('beta\n');
  process.stderr.write('diagnostic\n');
  process.exitCode = 3;
`], { stdio: ['ignore', 'pipe', 'pipe'] });

// Register immediately: once() also rejects if the child emits 'error'.
const completion = once(child, 'close');
async function drain(stream) {
  stream.setEncoding('utf8');
  let text = '';
  for await (const chunk of stream) text += chunk;
  return text;
}

const [stdout, stderr, [code, signal]] = await Promise.all([
  drain(child.stdout), drain(child.stderr), completion,
]);
assert.equal(stdout, 'alpha\nbeta\n');
assert.equal(stderr, 'diagnostic\n');
assert.equal(code, 3);
assert.equal(signal, null);
console.log('stdout:', JSON.stringify(stdout));
console.log('stderr:', JSON.stringify(stderr));
console.log('Exit code:', code);
console.log('Nonzero status detected:', code !== 0);

Run it:

node stream-child.mjs

Observed output:

stdout: "alpha\nbeta\n"
stderr: "diagnostic\n"
Exit code: 3
Nonzero status detected: true

Read the result

The two async consumers run concurrently, so neither pipe is left unread while the other fills. Chunks are arbitrary pieces of a byte stream, not complete lines or one chunk per child write. setEncoding handles UTF-8 boundaries, and this small fixture joins the chunks before printing deterministic results.

Waiting for close accounts for the child process ending and its stdio closing. A nonzero exit does not make spawn throw or automatically reject the event promise; inspect code and signal. The assertion deliberately expects code 3, so the parent demonstration exits successfully after verifying the failure case. A launch failure instead rejects completion through the child’s error event.

For large output, process chunks incrementally or pipe them to a destination instead of accumulating strings as this tiny fixture does. Bound runtime and output for real jobs. No shell is used here, and the embedded script is fixed trusted source; do not insert untrusted text into that script.

The official Node.js reference describes the API. The assertions above check the specific fixture and its intended edge case.

Clean up

Remove the source and then the empty practice directory:

rm -- stream-child.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If the directory is not empty, inspect the remaining files rather than deleting unknown contents.

Acknowledgements

This post was created with assistance from AI.

Last updated on