午前2時の本番クラッシュ
ローカル環境では完璧に動いていたのに、新しいPDFジェネレーターや画像処理ツールをサーバーにデプロイした瞬間、ログが爆発しました。Node.jsバックエンドが、たった一つの苛立たしいエラーを吐き出しています:
Error: spawn ENOENT
at Process.ChildProcess._handle.onexit (node:internal/child_process:283:19)
at onErrorNT (node:internal/child_process:476:16)
タスクは失敗し、プロセスはハングし、エラーメッセージは不親切なほど短いです。システムプログラミングにおいて、ENOENTは「Error NO ENTry(エントリーなし)」を意味します。つまり、Node.jsがOSに特定のプログラムを起動するよう命令したものの、OSがそのファイルを見つけられなかったということです。ファイルシステム上の404エラーに相当します。
Node.jsがコマンドを見つけられない理由
child_process.spawnを呼び出すと、Node.jsは新しいプロセスの起動を試みます。OSがENOENTを返した場合、指定した実行ファイルがシステムのPATHに存在しないか、指定したパスにタイポがあることを意味します。
これは主に3つの理由で発生します:
- Windowsの拡張子の罠: Windowsでは、
npm、git、condaなどのコマンドは実際には.exeファイルではありません。多くの場合、.cmdや.batスクリプトです。デフォルトでは、spawnはバイナリを探します。npm.exeが見つからなければ、npm.cmdがそこにあっても諦めてしまいます。 - 最小限の環境: Dockerコンテナや CI/CDランナーが「slim」イメージを使っている場合があります。例えば、
alpineLinuxイメージには、Dockerfileで明示的にインストールしない限り、git、ffmpeg、pythonは含まれていません。 - 相対パスの混乱:
./scripts/sync.shのスクリプトを実行しようとしていても、Nodeプロセスの実際の作業ディレクトリがスクリプトのあるフォルダではなく、プロジェクトルートになっている場合があります。
クイックフィックス1:シェルを有効にする
最も手っ取り早い解決策は、コマンドをシェル内で実行することです。これにより、ターミナルにコマンドを入力したときと同じように、OSがエイリアスやファイル拡張子を自動的に解決できるようになります。
const { spawn } = require('child_process');
// Windowsでは'npm'(拡張子なし)を探すため、これはよく失敗する
// const ls = spawn('npm', ['-v']);
// 修正:shellオプションを使う
const ls = spawn('npm', ['-v'], {
shell: true
});
ls.on('error', (err) => {
console.error('Subprocess failed to start:', err);
});
注意: ユーザーが入力した値をコマンドに渡している場合はshell: trueに注意してください。シェルインジェクション攻撃の危険性があります。
クイックフィックス2:Windows拡張子を手動で処理する
フルシェルを起動するオーバーヘッドを避けたい場合は、OSを手動で判定することができます。これはシェル方式よりもパフォーマンスが高く、プロセスの実行を厳密に保てます。
const { spawn } = require('child_process');
// OSに応じて正しいコマンド名を決定する
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const child = spawn(command, ['install']);
プロの解決策:'cross-spawn'を使う
すべてのシステムコマンドにif/elseロジックを書くのは面倒でミスも起きやすいです。業界標準はcross-spawnパッケージを使うことです。ネイティブのspawnのドロップイン代替として機能し、Windowsの癖をすべて自動的に処理してくれます。
まずインストールします:
npm install cross-spawn
次に、コードを更新します:
const spawn = require('cross-spawn');
// Windows、Linux、macOSで完璧に動作する
const child = spawn('npm', ['install'], { stdio: 'inherit' });
child.on('error', (err) => {
console.error('If this fails, npm is likely missing from your system PATH.');
});
環境のデバッグ
それでもENOENTが出る場合、実行しようとしているツール自体がインストールされていない可能性があります。プロセスをspawnする直前にシステムのPATHをログ出力することで、Node.jsが「見ている」ものをデバッグできます。
// PATHをログしてOSがバイナリを探している場所を確認する
console.log('Current System PATH:', process.env.PATH);
// スクリプトを実行する前に存在確認を行う
const fs = require('fs');
const scriptPath = './scripts/worker.sh';
if (!fs.existsSync(scriptPath)) {
console.error(`File not found at: ${scriptPath}`);
}
Docker環境では、ビルドステップを再確認してください。ffmpegをspawnして動画を処理しているなら、DockerfileにRUN apt-get update && apt-get install -y ffmpegのような行が必要です。それがなければ、どれだけコードを修正してもENOENTエラーは解消されません。
修正の確認方法
errorイベントとcloseイベントを監視して修正を確認します。spawnが成功した場合、即座に'error'イベントは発火しません。
const { spawn } = require('child_process');
const child = spawn('git', ['--version'], { shell: true });
child.on('error', (err) => {
console.error('❌ Fix failed. Error details:', err.message);
});
child.on('close', (code) => {
if (code === 0) {
console.log('✅ Success: The command was found and executed.');
} else {
console.log(`⚠️ Command found, but it failed with exit code ${code}`);
}
});
ゼロ以外の終了コードが表示されてもENOENTが出なければ、おめでとうございます。パスの問題は解決されました。あとはコマンドの引数を修正するだけです。

