The ProblemIt is a common sight in CI/CD logs: your build pipeline crashes just as it is trying to tidy up. You see a message that looks like this:
Error: ENOTEMPTY: directory not empty, rmdir '/app/dist'
This error triggers when your code attempts to delete a folder using a method that expects it to be empty. If even one tiny file remains, the operating system blocks the request. In a production environment, this often stalls deployments and leaves your build environment in a broken state.
Why Node.js Throws This ErrorThe original fs.rmdir() function is strict by design. It only removes folders that contain zero files or subfolders. Even a single 6KB hidden file, like a macOS .DS_Store or a Git .keep file, will cause the operation to fail immediately.
You will usually run into this for three reasons:
- Active Writes: A build tool like Vite or Webpack is still writing assets when the cleanup starts.- Hidden Files: The OS generated system files that your code didn't account for.- Race Conditions: One part of your script is adding a file while another tries to wipe the directory.## The Best Solution: Use fs.rmIf you are using Node.js version 14.14.0 or newer, stop using
fs.rmdirfor cleanup tasks. The modernfs.rmmethod includes arecursiveflag that works exactly likerm -rfin a terminal. It handles nested files and folders without complaining.
Synchronous Approach:```
const fs = require('fs');
try { fs.rmSync('/app/dist', { recursive: true, force: true }); console.log('Dist folder cleared.'); } catch (err) { console.error('Cleanup failed:', err.message); }
### Asynchronous (Promises) Approach:```
const fs = require('fs').promises;
async function safeDelete(targetPath) {
try {
await fs.rm(targetPath, { recursive: true, force: true });
} catch (err) {
console.error(`Could not delete ${targetPath}:`, err);
}
}
The force: true option is essential here. It ensures your script doesn't crash if the directory is already gone, making your build scripts much more resilient.
Handling Legacy EnvironmentsYou might be working on an older stack where fs.rm isn't available. In these cases, the rimraf package is the most reliable alternative. It has been the go-to tool for cross-platform deletion for years.
Install it via npm:
npm install rimraf
Then implement it in your code:
const rimraf = require('rimraf');
rimraf('/app/dist', (err) => {
if (err) console.error('Rimraf error:', err);
else console.log('Successfully wiped dist');
});
Troubleshooting Stubborn FoldersSometimes the error persists even with recursive deletion. This usually means a process has a "lock" on a file inside that folder. The OS physically prevents deletion while a file is in use.
1. Identify Locked FilesOn Linux or macOS, you can find out which process is holding the folder hostage. Open your terminal and run:
lsof +D /app/dist
If a Process ID (PID) appears, you must stop that task before you can delete the folder.
2. Fix CI/CD Race ConditionsIn Docker or GitHub Actions, ensure your "clean" and "build" steps aren't running simultaneously. If a parallel process creates a file exactly when rm is scanning the directory, you will hit the ENOTEMPTY error.
3. Windows File LocksWindows is particularly aggressive with file locks. If you have the /dist folder open in File Explorer or a VS Code terminal is currently inside that directory, the deletion will fail. Close all handles to the folder and try again.
Final VerificationAlways verify the deletion in your scripts to avoid silent failures later in the pipeline. A simple check ensures your environment is actually ready for the next build step.
const fs = require('fs');
const path = './dist';
fs.rmSync(path, { recursive: true, force: true });
if (!fs.existsSync(path)) {
console.log('โ
Clean state confirmed.');
} else {
console.error('โ Folder still exists. Check for file locks.');
process.exit(1);
}

