promisify-child-process

Drop-in async/await wrappers for Node's spawn, exec, fork, and execFile that stay real ChildProcess instances.

Library
npm
v5.0.1
63stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
31/100Needs Attention
Development Activity4
Maintenance20
Community28
Maturity60
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
70/100Good
Architecture72
Code Quality78
Innovation55
Learning Curve75

promisify-child-process wraps Node.js’s built-in child_process module so that spawn, exec, fork, and execFile can be awaited directly, without giving up any of the underlying ChildProcess API. Instead of returning a plain Promise, each call returns the same ChildProcess object with .then(), .catch(), and .finally() attached, so callers can still write to stdin, pipe stdout, or listen for events while also await-ing the result.

The library is a small, focused replacement for the standard callback-based child_process API. It resolves only when a process exits with code 0, and rejects with a structured error (carrying code, signal, stdout, and stderr) for non-zero exits, signals, or 'error' events. Output capturing for spawn/fork is opt-in via encoding or maxBuffer, matching the semantics of exec/execFile, which capture by default.

It ships as a dual CJS/ESM package with full TypeScript types, including conditional types that infer whether stdout/stderr are present based on the stdio and encoding options passed in. A promisifyChildProcess helper is also exported for wrapping ChildProcess instances created by other libraries.

What You Get

  • Promise-friendly spawn, exec, fork, and execFile that are drop-in replacements for child_process’s originals
  • Returned objects are still real ChildProcess instances — stdin, stdout, stderr, and events all work exactly as before
  • Structured rejection errors carrying code, signal, stdout, and stderr for non-zero exits, kills, or 'error' events
  • Opt-in output capturing via encoding or maxBuffer for spawn/fork, matching exec/execFile defaults
  • A standalone promisifyChildProcess wrapper for promisifying ChildProcess instances created by other code
  • Dual CJS/ESM build with TypeScript types that conditionally infer stdout/stderr presence from the options passed in

Common Use Cases

  • Build scripts and CLIs that need to await a subprocess’s exit while still streaming its output live
  • Test harnesses and task runners that shell out to external tools and need clean async error handling on failure
  • Wrapping third-party APIs that hand back a raw ChildProcess so the rest of a codebase can await it consistently
  • Migrating existing child_process-based code to async/await with a minimal, drop-in API change

Under The Hood

Architecture The entire library lives in a single module, src/index.ts, organized around one core primitive: promisifyChildProcess(child, options), which attaches a Promise to an existing ChildProcess instance via Object.create(child, {...}) — copying then/catch/finally onto a new object whose prototype is the original child, so the result remains instanceof ChildProcess while also being thenable. spawn and fork are thin wrappers that call the native child_process equivalents and pipe the result through this primitive; exec and execFile are built through a second higher-order function, promisifyExecMethod, that adapts the callback-style native APIs (which hand back (err, stdout, stderr)) into the same resolve/reject/attach pattern. Output capture is implemented manually with a bounded buffer (capture() closure tracking bufferSize against maxBuffer) rather than delegating to a stream utility, so the module has zero runtime dependencies beyond @babel/runtime. There is no plugin system or extensibility surface beyond the exported functions — the design intentionally stays a thin, single-purpose layer over Node’s own API.

Tech Stack Written in TypeScript targeting Node >=16, published as a dual ESM/CJS package (exports map pointing at dist/index.js and dist/index.cjs, with separate .d.ts/.d.cts type entries). The one runtime dependency is @babel/runtime, used for compiled helper functions. Development tooling is built entirely on the @jcoreio/toolchain family (toolchain, toolchain-typescript, toolchain-mocha, toolchain-circle, toolchain-semantic-release) — a shared internal toolchain the author maintains across their packages — which wires up TypeScript builds, ESLint 9 flat config, Prettier, and CircleCI. Releases are automated via semantic-release, and the package is explicitly published only from the built dist directory, never directly from source (prepublishOnly intentionally fails to enforce this).

Code Quality Tests use Mocha with Chai assertions and nyc for coverage, and are substantial relative to the library’s size — test/index.ts runs to over 500 lines, exercising spawn, fork, exec, and execFile across success, non-zero exit, signal-kill, maxBuffer-exceeded, and stdio-capture-disabled scenarios, using real child scripts written to disk in a before() hook rather than mocks. TypeScript is used throughout with fairly elaborate conditional types (ChunkTypeHelper, IsPipeHelper) to make the return type of each call reflect the stdio/encoding options statically. ESLint (flat config) and Prettier are both configured and run through the shared toolchain, and CI runs on CircleCI with coverage reporting to Codecov. No obvious gaps in error handling were found — both the event-based and callback-based code paths correctly surface code, signal, and captured output on failure.

What Makes It Unique The library’s specific technical bet is preserving full ChildProcess API compatibility while adding promise semantics — most alternatives (including the README’s own recommended alternative, execa) return a plain object or a Promise-only wrapper, losing direct access to stdin/stdout streams unless the library re-exposes them explicitly. Here, Object.create(child, {...}) means the returned value passes instanceof ChildProcess checks and supports every native method and event unmodified, so it can be handed to other code expecting a real ChildProcess while still being awaitable. The conditional-type return shape (stdout/stderr typed as undefined unless capturing is actually enabled) is also a deliberate, narrow piece of type-level engineering rather than a broad feature — it is not attempting to be a general process-management framework, and the README candidly frames it as a smaller, more conformant alternative to execa rather than a superset of it.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search