Introduction

The spawn ENOENT error occurs when Node.js child_process.spawn() or child_process.exec() cannot find the command to execute. Unlike exec() which runs commands through a shell (allowing PATH resolution), spawn() looks for the executable directly and does not search the PATH by default on all platforms. This error is common in production environments where the PATH differs from development, when using relative command names like ffmpeg instead of absolute paths, or when running inside Docker containers with minimal base images that lack common utilities.

Symptoms

``` node:events:491 throw er; // Unhandled 'error' event ^

Error: spawn ffmpeg ENOENT at ChildProcess._handle.onexit (node:internal/child_process:284:19) at onErrorNT (node:internal/child_process:477:16) at processTicksAndRejections (node:internal/process/task_queues:83:21) Emitted 'error' event on ChildProcess instance at: at ChildProcess._handle.onexit (node:internal/child_process:290:12) ```

Or with absolute paths:

bash
Error: spawn /usr/local/bin/my-command ENOENT
    at Process.ChildProcess._handle.onexit (node:internal/child_process:284:19)
    at onErrorNT (node:internal/child_process:477:16)

Common Causes

  • Command not in PATH: The executable is not in any directory listed in process.env.PATH
  • Using spawn with shell command syntax: spawn('ls -la') fails because spawn expects command and args separately
  • Docker minimal image missing tools: node:alpine does not include bash, curl, or other common tools
  • Windows requires .exe extension: spawn('python') works on Linux but spawn('python.exe') on Windows
  • Relative path to command: spawn('./scripts/run.sh') fails if the file is not executable
  • File permission denied: Command exists but is not executable (no +x permission)

Step-by-Step Fix

Step 1: Use absolute paths for commands

```javascript const { spawn } = require('child_process');

// WRONG - relies on PATH const child = spawn('ffmpeg', ['-i', 'input.mp4', 'output.mp3']);

// CORRECT - use absolute path const child = spawn('/usr/bin/ffmpeg', ['-i', 'input.mp4', 'output.mp3']);

// Or find the path dynamically const { execSync } = require('child_process'); const ffmpegPath = execSync('which ffmpeg').toString().trim(); const child = spawn(ffmpegPath, ['-i', 'input.mp4', 'output.mp3']); ```

Step 2: Pass PATH environment to spawn

```javascript const { spawn } = require('child_process');

const child = spawn('ffmpeg', ['-i', 'input.mp4', 'output.mp3'], { env: { ...process.env, PATH: ${process.env.PATH}:/usr/local/bin:/opt/ffmpeg/bin, }, stdio: ['pipe', 'pipe', 'pipe'], });

child.stderr.on('data', (data) => { console.error(data.toString()); });

child.on('close', (code) => { console.log(ffmpeg exited with code ${code}); });

// ALWAYS handle the error event child.on('error', (err) => { console.error(Failed to spawn process: ${err.message}); }); ```

Step 3: Use shell option for command resolution

```javascript const { spawn } = require('child_process');

// Use shell: true to let the shell resolve the command const child = spawn('ffmpeg -i input.mp4 output.mp3', { shell: true, stdio: 'inherit', });

// Or use exec for shell commands const { exec } = require('child_process'); exec('ffmpeg -i input.mp4 output.mp3', (error, stdout, stderr) => { if (error) { console.error(exec error: ${error.message}); return; } console.log(stdout); }); ```

Step 4: Check command availability before spawning

```javascript const { access } = require('fs/promises'); const { spawn } = require('child_process');

async function isExecutable(cmd) { try { await access(cmd, access.constants.X_OK); return true; } catch { return false; } }

async function runCommand(cmd, args) { if (!await isExecutable(cmd)) { throw new Error(Command not found or not executable: ${cmd}); }

return new Promise((resolve, reject) => { const child = spawn(cmd, args); let stdout = ''; let stderr = '';

child.stdout.on('data', (data) => { stdout += data; }); child.stderr.on('data', (data) => { stderr += data; });

child.on('close', (code) => { if (code === 0) { resolve({ stdout, stderr }); } else { reject(new Error(Command exited with code ${code}: ${stderr})); } });

child.on('error', reject); }); } ```

Prevention

  • Always handle the error event on child processes to catch ENOENT
  • Use absolute paths or verify command existence before spawning
  • Include required system dependencies in Docker images
  • Use shell: true when you need shell resolution (but be aware of shell injection risks)
  • Add a startup check that verifies all required external commands are available
  • Log process.env.PATH at startup to debug PATH-related issues
  • Use npx or which to resolve command paths dynamically