Fix: Error: spawn ENOENT (Node.js child_process)
Part of: JavaScript & TypeScript Errors
Quick Answer
Fix Node.js 'Error: spawn ENOENT' from child_process.spawn(): PATH resolution, the Windows .cmd/.bat problem, and why shell: true is now discouraged (DEP0190).
The confusing part is that the exact same command works fine when you type it into a terminal yourself. That gap, works by hand, fails from spawn(), is the whole story of this error: a shell does a lot of invisible work resolving what you type, and spawn() deliberately skips almost all of it. I’ve debugged this exact mismatch on a CI matrix where a build step passed on Linux and macOS runners and failed only on Windows, for a reason that had nothing to do with the command itself. This covers PATH resolution, the Windows .cmd/.bat problem specifically, and a very recent Node change that makes the old go-to fix, shell: true, the wrong answer more often than it used to be.
Error: spawn ENOENT
Your Node.js process throws this when child_process.spawn() (or execFile, which is built on the same mechanism) tries to launch a command:
Error: spawn eslint ENOENT
at ChildProcess._handle.onexit (node:internal/child_process:283:19)
at onErrorNT (node:internal/child_process:483:16)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
errno: -2,
code: 'ENOENT',
syscall: 'spawn eslint',
path: 'eslint',
spawnargs: [ '--fix' ]
}The path and spawnargs properties are the fastest way to see exactly what Node tried to execute and with what arguments, print them before you guess:
child.on('error', (err) => {
console.error(`Failed to launch: ${err.path}`, err.spawnargs, err.code);
});Why this happens
spawn() calls the operating system’s process-creation API almost directly, it does not run your command through a shell first. That distinction matters more than it looks. When you type a command into a terminal, the shell does real work before anything executes: it searches PATH using the extensions your OS considers executable, expands aliases, resolves shell built-ins, and on Windows specifically, knows to run a .cmd or .bat wrapper file even though you only typed the bare command name. spawn() skips essentially all of that. It looks for a file matching the exact name you gave it, and if the OS can’t find and directly execute that file, you get ENOENT, the same code used for a missing file anywhere else in Node.
So this error is never really about a missing file on disk in the way fs.readFile throwing ENOENT is. It means the specific executable Node tried to launch, by that exact name, was not something the OS could directly exec, even if the same name works perfectly when you type it yourself.
Fix a typo or a PATH problem first
Rule out the obvious before anything Windows-specific. Confirm the command resolves at all in your current shell:
which eslint # macOS / Linuxwhere.exe eslint # WindowsIf that comes back empty, the binary genuinely isn’t installed or isn’t on PATH for the account and environment your Node process runs under, install it, or use an absolute path in your spawn() call. This is especially common in CI, systemd services, and cron jobs, which often run with a stripped-down PATH that doesn’t match your interactive shell’s.
If the tool is a project dependency rather than a global install, confirm you’re pointing at node_modules/.bin, not assuming it’s on the system PATH at all:
const path = require('node:path');
const eslintPath = path.join(__dirname, 'node_modules', '.bin', 'eslint');
spawn(eslintPath, ['--fix']);Fix the Windows .cmd/.bat problem
This is the single most common cause, and it only shows up on Windows, which is exactly why it slips through code that was only ever tested on macOS or Linux. Almost every npm-installed CLI tool installs as a .cmd (or .ps1) wrapper script on Windows, not a raw executable matching the bare command name. spawn('eslint', [...]) looks for a file literally named eslint, finds nothing, and throws ENOENT, even though eslint runs fine when you type it into PowerShell or cmd.exe yourself, because the shell itself is doing the .cmd resolution that spawn() skips.
Node’s own documentation is explicit that .bat and .cmd files cannot be launched with execFile() at all, and lists the ways to launch one with spawn() or exec(). The commonly recommended fix used to be simple:
// The old fix: works, but see the warning below
spawn('eslint', ['--fix'], { shell: true });Recent Node versions deprecate this specific pattern. Passing an args array together with shell: true is now flagged as DEP0190, because the array elements only get space-joined, not properly shell-escaped, which is a real command-injection risk if any argument comes from user input. You will see a DeprecationWarning: DEP0190 in the console even when the command itself still works.
The safer, currently recommended alternatives:
Spawn cmd.exe directly, which does not set shell: true at all, and Node escapes the argument array correctly for you:
spawn('cmd.exe', ['/c', 'eslint', '--fix']);Or use the cross-spawn package, which is what many tools (including npm itself) use internally specifically to solve this without touching shell: true:
npm install cross-spawnconst spawn = require('cross-spawn');
spawn('eslint', ['--fix']); // resolves .cmd/.bat on Windows automatically, no shell option neededIf you genuinely need shell features, pipes, redirection, environment variable expansion in the command itself, put the entire command in a single string instead of an array, rather than combining shell: true with an args array:
// Avoids DEP0190: one string, no separate args array
spawn('eslint --fix', { shell: true });Fix relative path resolution
A relative path resolves against process.cwd() by default, not the directory your script file lives in, and not necessarily the directory you expect when the process is launched by a task runner, systemd, or a process manager:
// BUG: assumes cwd is the project root, which isn't guaranteed
spawn('./scripts/build.sh');// FIX: resolve against the script's own location, not cwd
const path = require('node:path');
spawn(path.join(__dirname, 'scripts', 'build.sh'));Or set cwd explicitly in the spawn() options so both the working directory and any relative path arguments behave predictably regardless of where the parent process was started from.
Fix missing execute permission or a broken shebang (macOS/Linux)
A script that exists but lacks the executable bit, or whose shebang line points at an interpreter that isn’t installed, both surface confusingly. Missing execute permission usually throws EACCES, not ENOENT, check err.code to tell them apart quickly:
chmod +x ./scripts/build.shA shebang pointing at an interpreter the OS can’t find, #!/usr/bin/env python on a system where only python3 exists, for example, causes the kernel to fail resolving the interpreter, which Node then reports as ENOENT for the script itself, not the missing interpreter. I’ve seen this exact case waste an hour of debugging effort aimed at Node’s spawn options when the actual fix was a one-line shebang edit. Run the script directly in a shell first; the shell’s own error message here is usually clearer than Node’s.
Fix a missing binary in Docker or CI
A multi-stage Docker build that copies application code into a slim final image, without also copying (or reinstalling) a binary the code shells out to, produces ENOENT only in the container, never locally where the tool happens to already be installed globally. Confirm the binary actually exists inside the exact image your app runs in, not your host machine:
docker run --rm your-image which imagemagickIf it comes back empty, install the dependency in the final stage, not only in an earlier build stage that gets discarded.
Still not working?
node_modules/.bin isn’t in the child’s PATH. npm run and npx add node_modules/.bin to PATH automatically for the process they launch, but a plain spawn() call from your own code does not inherit that unless you’re already running inside an npm script. I’ve hit this specifically inside a custom build script that shelled out to a locally installed CLI tool directly instead of going through an npm script, and it worked for every teammate who happened to also have the tool installed globally. Either use the full path to the binary, or explicitly extend the env.PATH you pass to spawn().
The command exists but is a symlink to something that doesn’t. A broken symlink resolves to ENOENT at exec time even though ls or a file browser might still show the link itself. Check with ls -la and confirm the link’s target actually exists.
It fails only in production, not in your local Docker Compose setup. A slightly different base image, a missing apt-get install layer that was cached locally but not rebuilt in CI, or a different processor architecture (ARM vs x86) pulling a different, incomplete package, are all common causes of this exact split. Rebuild the image from a clean cache before trusting a “works on my machine” result.
You’re seeing ENOENT from an entirely different Node API, not child_process. fs.readFile, fs.stat, and similar filesystem calls throw the same ENOENT code for a missing file, with a different syscall value (open, stat, and so on, instead of spawn <command>). Check err.syscall to confirm which one you’re actually looking at before chasing the wrong fix.
For related command and module resolution errors, see Fix: Linux command not found, Fix: Node.js ERR_MODULE_NOT_FOUND, and Fix: npm ERR! code ELIFECYCLE.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Cannot set headers after they are sent to the client
Fix Node.js/Express 'Cannot set headers after they are sent to the client', usually a missing return, double next(), or a duplicate response.
Fix: TypeError: fetch failed (Node.js)
Fix Node.js 'TypeError: fetch failed' by reading error.cause to find what really failed: DNS, a refused connection, a timeout, or a bad certificate.
Fix: Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
Fix Node.js 'Error [ERR_REQUIRE_ESM]: require() of ES Module not supported' by checking your Node version, using dynamic import(), or converting to ESM.
Fix: listen EADDRINUSE: address already in use (Node.js)
Fix Node.js 'listen EADDRINUSE: address already in use': find and kill the process on the port (macOS, Linux, Windows), or run your server on a different port.