execPromis
-style shell execution with captured stdoutIn Node (v14 +) the usual way to run a shell command and await its output is:
// 1️⃣ pull in the core pieces
import { exec as _exec } from 'node:child_process';
import { promisify } from 'node:util';
// 2️⃣ lift exec into the Promise world
const execPromis = promisify(_exec);
/**
* 3️⃣ convenience wrapper — resolves with text, rejects on non-zero exit.
* @param {string} cmd Shell command line (quoted exactly as you would in a terminal)
* @param {object} [opts] Any child_process.exec
options (cwd, env, timeout, maxBuffer …)
* @returns {Promise<string>} Resolves to stdout; errors carry stderr, code
and signal
.
*/
export async function run(cmd, opts = {}) {
try {
const { stdout } = await execPromis(cmd, {
// 🔸 4 MB default is tiny if you git-diff or cat large files:
maxBuffer: 64 * 1024 * 1024,
...opts,
});
return stdout; // 🎉 capture succeeded
} catch (err) {
// attach a more ergonomic message
err.message = `Command "${cmd}" failed: ${err.message}\n--- stderr ---\n${err.stderr}`;
throw err; // 🛑 propagate for caller to handle
}
}
child_process.exec
starts one full shell, buffers stdout/stderr in memory and hands everything back at once when the process exits.util.promisify
turns the node-style callback (err, result)
into a clean Promise
.maxBuffer
override – avoids the common “stdout maxBuffer exceeded” crash when output > 1 MiB.Error
carries
stderr
, code
(numeric exit status) and signal
(kill signal if any).
try {
const listing = await run('ls -lah ~/Downloads');
console.log('Dir contains:\n', listing);
} catch (e) {
console.error(e.message); // 1️⃣ human-readable summary
console.error(e.stderr); // 2️⃣ raw stderr from the command
console.error(e.code); // 3️⃣ exit code (≠ 0 → failure)
}
opts
Option | Type / Default | What it controls |
---|---|---|
cwd | string | process.cwd() | Working directory for the spawned shell. |
env | object | process.env | Custom environment variables. |
timeout | number (ms) | 0 | Hard kill after <timeout> milliseconds. |
maxBuffer | number (B) | 1 MiB | Total bytes allowed for each of stdout /stderr . |
shell | string | platform default | Explicit shell (e.g. /bin/zsh , powershell.exe ). |
exec()
is not idealchild_process.spawn
and stream line-by-line instead.stdin
need spawn
with pipes or a PTY lib (node-pty
).Buffer
, again easier with spawn
.
import { spawn } from 'node:child_process';
// Stream example: tail -f
const tail = spawn('tail', ['-f', '/var/log/system.log'], { stdio: 'pipe' });
tail.stdout.on('data', chunk => process.stdout.write(chunk));
tail.on('close', code => console.log('tail exited', code));
execPromis
is just util.promisify(exec)
– nothing magical, but very handy for concise async/await flows.maxBuffer
for commands with heavy output.spawn
.{ stdout, stderr, code }
when you need richer diagnostics; return only stdout when a quick string is enough.