Skip to content
JavaScript Basics: Run Small Commands with execFile

JavaScript Basics: Run Small Commands with execFile

October 23, 2026

A Node.js script sometimes needs a result from another executable. execFile runs a program with a separate argument list and collects its output. These examples launch the current Node.js executable, so no extra command-line program needs to be installed.

These three independent examples were executed on Linux with Node.js v24.19.0. Use Node.js 24 for this tutorial; only the installed version above was tested. They use built-in modules, make no network requests, and assert their results. The output below is exact, not a sketch.

Prepare a scratch directory

You need Bash, Node.js on your PATH, a text editor, and the mktemp, rm, and rmdir utilities. Keep the same terminal open and save each named file as UTF-8 in the new directory. The .mjs extension selects an ES module.

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

The tested runtime prints:

v24.19.0

1. Pass an argument without shell interpretation

Save this as pass-literal-process-argument.mjs:

import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const runFile = promisify(execFile);
const label = 'weekly report; still one argument';
const { stdout, stderr } = await runFile(process.execPath, [
  '-e', 'console.log(process.argv[1])', '--', label
], { encoding: 'utf8', shell: false, timeout: 5000, maxBuffer: 4096 });
assert.equal(stdout, label + '\n');
assert.equal(stderr, '');
console.log(stdout.trimEnd());

Run it:

node pass-literal-process-argument.mjs

Output:

weekly report; still one argument

The spaces and semicolon remain part of one argument; they do not create another command. process.execPath selects the running Node executable rather than searching PATH. The double hyphen ends Node option parsing before the value. The code passed to -e is fixed application code, never user input. Keeping the argument separate does not prevent the target program from interpreting its own options or data dangerously. See execFile.

2. Separate machine-readable output from diagnostics

Save this as capture-child-output-streams.mjs:

import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const runFile = promisify(execFile);
const script = [
  'console.log(JSON.stringify({ rows: 3, valid: true }));',
  'console.error("checked local sample");'
].join('\n');
const { stdout, stderr } = await runFile(process.execPath, ['-e', script], {
  encoding: 'utf8', timeout: 5000, maxBuffer: 4096
});
const result = JSON.parse(stdout);
assert.deepEqual(result, { rows: 3, valid: true });
assert.equal(stderr, 'checked local sample\n');
console.log(`rows: ${result.rows}; valid: ${result.valid}`);
console.log(`diagnostic: ${stderr.trimEnd()}`);

Run it:

node capture-child-output-streams.mjs

Output:

rows: 3; valid: true
diagnostic: checked local sample

The child uses stdout for JSON and stderr for a human-readable diagnostic. Writing to stderr alone does not mean the command failed: this child exits successfully. Parsing only stdout avoids mixing log messages into the data format. promisify gives execFile its documented promise result containing both streams, rather than just the first callback value.

3. Handle a nonzero exit as a rejected promise

Save this as handle-child-exit-code.mjs:

import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const runFile = promisify(execFile);
let failure;
try {
  await runFile(process.execPath, [
    '-e', 'console.error("sample rejected"); process.exitCode = 7;'
  ], { encoding: 'utf8', timeout: 5000, maxBuffer: 4096 });
} catch (error) {
  failure = error;
}
assert.ok(failure);
assert.equal(failure.code, 7);
assert.equal(failure.stdout, '');
assert.equal(failure.stderr, 'sample rejected\n');
console.log(`exit code: ${failure.code}`);
console.log(`diagnostic: ${failure.stderr.trimEnd()}`);

Run it:

node handle-child-exit-code.mjs

Output:

exit code: 7
diagnostic: sample rejected

The child sets exitCode instead of forcing an immediate process exit, allowing its output to drain normally. The parent captures the expected rejection and verifies the numeric status and diagnostic. Other failures have different shapes: a missing executable can produce an ENOENT string code, while signal termination can include a signal. Do not assume every rejection has a numeric code. Node documents promise rejection output alongside the execFile callback and promise behavior.

Practical limits

execFile buffers stdout and stderr; maxBuffer limits bytes in each stream, not characters. For large or incremental output, prefer spawn and consume its streams. A timeout sends a termination signal, not a universal guarantee that an arbitrary program stops immediately. A child inherits substantial operating-system access, so run trusted executables and validate inputs. Windows command scripts have additional shell requirements; these examples were tested on Linux.

Clean up

In the same terminal, remove only the three named source files, return to your starting directory, and remove the now-empty scratch directory:

rm -- pass-literal-process-argument.mjs capture-child-output-streams.mjs handle-child-exit-code.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on