A quick guide to execPromis-style shell execution with captured stdout

In 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
  }
}

How it works 🔍

Example usage 🚀


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)
}

Common parameters you can pass in opts

OptionType / DefaultWhat it controls
cwdstring | process.cwd()Working directory for the spawned shell.
envobject | process.envCustom environment variables.
timeoutnumber (ms) | 0Hard kill after <timeout> milliseconds.
maxBuffernumber (B) | 1 MiBTotal bytes allowed for each of stdout/stderr.
shellstring | platform defaultExplicit shell (e.g. /bin/zsh, powershell.exe).

When exec() is not ideal

  1. Huge output – 100 MB logs will bloat RAM before you touch them. Use child_process.spawn and stream line-by-line instead.
  2. Interactive commands – things that ask for passwords or use stdin need spawn with pipes or a PTY lib (node-pty).
  3. Binary data – if the command emits non-UTF-8 (images, archives) you’ll want a 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));

Take-aways 🎯