Cách khắc phục lỗi 'ENOTEMPTY' trong Node.js (rmdir)

beginner💚 Node.js2026-06-13| Node.js (v14.14.0+), Linux, macOS, Windows, Docker, GitHub Actions, Jenkins

Error Message

Error: ENOTEMPTY: directory not empty, rmdir '/app/dist'
#fs#nodejs#javascript#devops#backend

Vấn đềĐây là một tình huống thường gặp trong nhật ký CI/CD: pipeline build của bạn gặp lỗi ngay khi đang cố gắng dọn dẹp. Bạn sẽ thấy một thông báo như sau:

Error: ENOTEMPTY: directory not empty, rmdir '/app/dist'

Lỗi này xảy ra khi mã nguồn của bạn cố gắng xóa một thư mục bằng phương thức yêu cầu thư mục đó phải trống. Nếu chỉ còn lại một tệp nhỏ, hệ điều hành sẽ chặn yêu cầu này. Trong môi trường production, điều này thường làm đình trệ việc triển khai và khiến môi trường build rơi vào trạng thái lỗi.

Tại sao Node.js lại đưa ra lỗi nàyHàm fs.rmdir() ban đầu được thiết kế rất nghiêm ngặt. Nó chỉ xóa các thư mục không chứa bất kỳ tệp hoặc thư mục con nào. Ngay cả một tệp ẩn dung lượng 6KB, như .DS_Store trên macOS hoặc tệp .keep của Git, cũng sẽ khiến thao tác thất bại ngay lập tức.

Bạn thường gặp lỗi này vì ba lý do sau:

  • Đang ghi dữ liệu: Một công cụ build như Vite hoặc Webpack vẫn đang ghi các tài nguyên khi quá trình dọn dẹp bắt đầu.- Tệp ẩn: Hệ điều hành tạo ra các tệp hệ thống mà mã nguồn của bạn không tính đến.- Race Conditions: Một phần của script đang thêm tệp trong khi phần khác lại cố gắng xóa thư mục.## Giải pháp tốt nhất: Sử dụng fs.rmNếu bạn đang sử dụng Node.js phiên bản 14.14.0 trở lên, hãy ngừng sử dụng fs.rmdir cho các tác vụ dọn dẹp. Phương thức fs.rm hiện đại bao gồm cờ recursive hoạt động chính xác như lệnh rm -rf trong terminal. Nó xử lý các tệp và thư mục lồng nhau mà không gặp lỗi.

Cách tiếp cận đồng bộ (Synchronous):```

const fs = require('fs');

try { fs.rmSync('/app/dist', { recursive: true, force: true }); console.log('Đã dọn dẹp thư mục dist.'); } catch (err) { console.error('Dọn dẹp thất bại:', err.message); }


### Cách tiếp cận bất đồng bộ (Promises):```
const fs = require('fs').promises;

async function safeDelete(targetPath) {
  try {
    await fs.rm(targetPath, { recursive: true, force: true });
  } catch (err) {
    console.error(`Không thể xóa ${targetPath}:`, err);
  }
}

Tùy chọn force: true là rất quan trọng ở đây. Nó đảm bảo script của bạn không bị crash nếu thư mục đã bị xóa trước đó, giúp các script build của bạn hoạt động ổn định hơn nhiều.

Xử lý các môi trường cũ (Legacy)Bạn có thể đang làm việc trên một hệ thống cũ hơn, nơi fs.rm không khả dụng. Trong những trường hợp này, package rimraf là lựa chọn thay thế đáng tin cậy nhất. Nó đã là công cụ phổ biến để xóa đa nền tảng trong nhiều năm qua.

Cài đặt qua npm:

npm install rimraf

Sau đó triển khai trong mã của bạn:

const rimraf = require('rimraf');

rimraf('/app/dist', (err) => {
  if (err) console.error('Lỗi Rimraf:', err);
  else console.log('Đã xóa sạch thư mục dist thành công');
});

Khắc phục sự cố các thư mục "ngoan cố"Đôi khi lỗi vẫn tiếp diễn ngay cả khi đã xóa đệ quy. Điều này thường có nghĩa là một tiến trình đang "khóa" (lock) một tệp bên trong thư mục đó. Hệ điều hành ngăn chặn việc xóa tệp khi nó đang được sử dụng.

1. Xác định các tệp bị khóaTrên Linux hoặc macOS, bạn có thể tìm ra tiến trình nào đang chiếm giữ thư mục. Mở terminal và chạy:

lsof +D /app/dist

Nếu ID tiến trình (PID) xuất hiện, bạn phải dừng tác vụ đó trước khi có thể xóa thư mục.

2. Khắc phục Race Conditions trong CI/CDTrong Docker hoặc GitHub Actions, hãy đảm bảo các bước "clean" và "build" không chạy đồng thời. Nếu một tiến trình song song tạo ra một tệp ngay khi rm đang quét thư mục, bạn sẽ gặp lỗi ENOTEMPTY.

3. Khóa tệp trên WindowsWindows đặc biệt nghiêm ngặt với việc khóa tệp. Nếu bạn đang mở thư mục /dist trong File Explorer hoặc một terminal của VS Code đang nằm trong thư mục đó, việc xóa sẽ thất bại. Hãy đóng tất cả các ứng dụng đang truy cập thư mục và thử lại.

Kiểm tra cuối cùngLuôn xác minh việc xóa trong script của bạn để tránh các lỗi âm thầm về sau trong pipeline. Một bước kiểm tra đơn giản sẽ đảm bảo môi trường của bạn thực sự sẵn sàng cho bước build tiếp theo.

const fs = require('fs');
const path = './dist';

fs.rmSync(path, { recursive: true, force: true });

if (!fs.existsSync(path)) {
  console.log('✅ Đã xác nhận trạng thái sạch.');
} else {
  console.error('❌ Thư mục vẫn tồn tại. Kiểm tra các tệp bị khóa.');
  process.exit(1);
}

Related Error Notes