Node.js child_process spawn ENOENT — Child Process Fails to Launch
Diagnose a Node.js child_process spawn ENOENT failure caused by a missing binary, a deleted cwd, or a PATH that differs between your shell and the service.
Symptoms
- Node throws "Error: spawn <cmd> ENOENT" from spawn() or execFile()
- The identical command succeeds when typed manually in a terminal
- Works on your laptop but fails inside Docker or a systemd unit
Possible causes
- The executable is not installed or is not on the PATH visible to the Node process
- cwd points at a directory that no longer exists, so a relative binary path cannot resolve
- A service unit starts with a stripped PATH that omits /usr/local/bin or version-manager shims
Diagnosis steps
Confirm the binary resolves in your own shell
command -v <cmd> && <cmd> --versionInterpretation
If the shell cannot find it either, install it first. If it resolves here but not from Node, the problem is PATH or cwd, not the install.
Dump the Node process PATH and cwd
node -e "console.log(process.env.PATH); console.log(process.cwd())"Interpretation
Compare against the shell PATH. Services commonly launch with a minimal PATH that excludes nvm or asdf shim directories.
Retry using an absolute path
node -e "require('child_process').spawnSync('/usr/bin/ffmpeg',['-version'],{stdio:'inherit'})"Interpretation
If the absolute path works, the binary exists and only PATH resolution is broken.
Root cause
spawn() without shell:true calls execvp and searches only the process PATH, never the login shell. When the binary is absent from that PATH, or cwd has disappeared so a relative path cannot be resolved, the kernel returns ENOENT and Node surfaces it as spawn ENOENT.
Fix
Resolve the executable to an absolute path (which/where) once at startup, pass an explicit env that carries the PATH you expect, set cwd to a directory you know exists, and use shell:true only for shell builtins or genuine command lines.
Risk note
shell:true combined with interpolated user input enables command injection; never concatenate arguments into the command string.
Verify the fix
Re-run the child process and confirm a zero exit code. Log the resolved binary path at startup and assert the file exists so a missing binary fails fast with a clear message instead of a cryptic ENOENT.
Symptoms
This guide profiles the failure behind “Node.js child_process spawn ENOENT — Child Process Fails to Launch”. It walks through the observable symptoms, a reproducible diagnosis path, the underlying root cause, the concrete fix, and how to verify the change and prevent a regression.
Observed symptoms include:
- Node throws “Error: spawn
ENOENT” from spawn() or execFile() - The identical command succeeds when typed manually in a terminal
- Works on your laptop but fails inside Docker or a systemd unit
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Confirm the binary resolves in your own shell
command -v <cmd> && <cmd> --version
If the shell cannot find it either, install it first. If it resolves here but not from Node, the problem is PATH or cwd, not the install.
Step 2: Dump the Node process PATH and cwd
node -e "console.log(process.env.PATH); console.log(process.cwd())"
Compare against the shell PATH. Services commonly launch with a minimal PATH that excludes nvm or asdf shim directories.
Step 3: Retry using an absolute path
node -e "require('child_process').spawnSync('/usr/bin/ffmpeg',['-version'],{stdio:'inherit'})"
If the absolute path works, the binary exists and only PATH resolution is broken.
Likely contributors, in order of frequency:
- The executable is not installed or is not on the PATH visible to the Node process
- cwd points at a directory that no longer exists, so a relative binary path cannot resolve
- A service unit starts with a stripped PATH that omits /usr/local/bin or version-manager shims
Root Cause
spawn() without shell:true calls execvp and searches only the process PATH, never the login shell. When the binary is absent from that PATH, or cwd has disappeared so a relative path cannot be resolved, the kernel returns ENOENT and Node surfaces it as spawn ENOENT.
The Fix
Resolve the executable to an absolute path (which/where) once at startup, pass an explicit env that carries the PATH you expect, set cwd to a directory you know exists, and use shell:true only for shell builtins or genuine command lines.
Watch out: shell:true combined with interpolated user input enables command injection; never concatenate arguments into the command string.
import { spawn, execFileSync } from 'node:child_process';
// Resolve once at boot, then reuse.
const bin = execFileSync('which', ['ffmpeg']).toString().trim();
const child = spawn(bin, ['-version'], {
stdio: 'inherit',
cwd: process.cwd(),
env: { ...process.env, PATH: '/usr/local/bin:/usr/bin:/bin' },
});
child.on('error', (err) => console.error('spawn failed:', err.code, bin));
Verify and Prevent
Re-run the child process and confirm a zero exit code. Log the resolved binary path at startup and assert the file exists so a missing binary fails fast with a clear message instead of a cryptic ENOENT.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.