Thông báo lỗi
Nếu bạn đã từng phát triển ứng dụng Node.js trên Windows, có lẽ bạn đã gặp phải lỗi EBUSY. Nó thường xuất hiện ngay khi bạn cố gắng xóa, đổi tên hoặc di chuyển một file, khiến bạn tự hỏi tại sao một thao tác đơn giản lại đột ngột bị cấm:
Error: EBUSY: resource busy or locked, unlink 'C:\project\data\temp.txt'
Tại sao lỗi này xảy ra
Về mặt kỹ thuật, EBUSY không phải là lỗi của Node.js; đó là thông báo trực tiếp từ hệ điều hành Windows. Khác với Linux hay macOS thường cho phép bạn xóa file ngay cả khi chúng đang mở, Windows thực thi cơ chế khóa file nghiêm ngặt. Nếu bất kỳ tiến trình nào đang giữ một "handle" (tay cầm) mở tới file đó, hệ điều hành sẽ từ chối yêu cầu chỉnh sửa của bạn.
Các nguyên nhân phổ biến bao gồm:
- Các Stream đang hoạt động: Một stream file (
readhoặcwrite) không được đóng đúng cách bằng.end(). - Lập chỉ mục của trình soạn thảo: VS Code hoặc IntelliJ đang lập chỉ mục các file dự án của bạn ở chế độ chạy nền.
- Quét phần mềm diệt virus: Windows Defender hoặc các công cụ bên thứ ba quét một file ngay miligiây sau khi nó được tạo.
- Các dịch vụ hệ thống: Windows Search Indexer hoặc khung xem trước (preview pane) của File Explorer đang giữ file mở.
- Race Conditions (Tranh chấp tài nguyên): Cố gắng xóa một file trong khi thao tác ghi trước đó vẫn đang được đẩy (flush) xuống đĩa.
Các bước khắc phục
1. Đóng các Stream một cách rõ ràng
Quên đóng stream là lý do phổ biến nhất gây ra lỗi EBUSY. Ngay cả khi bạn đã ghi xong dữ liệu, Node.js có thể vẫn giữ handle của file cho đến khi bộ thu gom rác (garbage collection) hoạt động. Quá trình này có thể mất vài giây. Để an toàn, hãy luôn đợi sự kiện close trước khi cố gắng xóa file.
const fs = require('fs');
const stream = fs.createWriteStream('example.txt');
stream.write('Dữ liệu để xử lý');
// Tín hiệu kết thúc việc ghi
stream.end();
// Chỉ xóa khi chúng ta chắc chắn 100% rằng hệ điều hành đã giải phóng khóa
stream.on('close', async () => {
try {
await fs.promises.unlink('example.txt');
console.log('Xóa file thành công');
} catch (err) {
console.error('Lỗi EBUSY kéo dài:', err);
}
});
2. Triển khai vòng lặp thử lại thông minh
Vì các khóa bên ngoài (như quét virus) thường là tạm thời, chúng thường kéo dài ít hơn 50ms. Một cơ chế thử lại đơn giản có thể vượt qua những "gián đoạn" nhỏ này và giữ cho ứng dụng của bạn chạy mượt mà mà không bị crash.
const fs = require('fs').promises;
async function safeUnlink(filePath, retries = 5, delay = 100) {
for (let i = 0; i setTimeout(res, delay));
continue;
}
throw err;
}
}
}
3. Sử dụng graceful-fs
Nếu ứng dụng của bạn thực hiện nhiều thao tác I/O file, hãy cân nhắc sử dụng graceful-fs. Đây là một thư viện thay thế trực tiếp cho module fs gốc, đã được kiểm chứng qua thời gian và được sử dụng bởi npm và jest. Nó tự động thử lại các thao tác thất bại do lỗi hệ điều hành tạm thời như EBUSY hoặc EMFILE.
// Cài đặt: npm install graceful-fs
const fs = require('graceful-fs');
fs.unlink('path/to/file', (err) => {
if (err) throw err;
console.log('Đã xóa thành công');
});
4. Xác định tiến trình đang khóa file
Nếu code của bạn đã chuẩn nhưng lỗi vẫn tiếp diễn, chắc chắn một chương trình khác đang giữ khóa. Bạn có thể sử dụng Resource Monitor của Windows để tìm ra thủ phạm:
- Nhấn
Win + R, gõresmonvà nhấn Enter. - Chuyển đến tab CPU.
- Trong hộp tìm kiếm Associated Handles, gõ tên file của bạn (ví dụ:
temp.txt). - Danh sách sẽ hiển thị chính xác tiến trình nào (như
MsMpEng.execủa Defender hoặcEverything.exe) là chủ sở hữu.
Xác minh và Phòng ngừa
Để đảm bảo cách khắc phục của bạn hiệu quả, hãy chạy một Bài kiểm tra áp lực (Stress Test) 100 lần: bọc thao tác file của bạn trong một vòng lặp và thực hiện nó 100 lần liên tiếp nhanh chóng. Các lỗi Race condition thường ẩn nấp khi chạy thủ công một lần nhưng sẽ xuất hiện ngay lập tức dưới tải trọng cao.
Để ngăn chặn những vấn đề này trong tương lai:
- Tránh các phương thức Sync (đồng bộ):
fs.unlinkSynclàm nghẽn event loop và dễ gặp xung đột khóa hơn so vớifs.promises. - Loại trừ các thư mục tạm: Nếu bạn tạo ra hàng ngàn bản ghi log nhỏ, hãy đặt chúng vào một thư mục riêng và loại trừ đường dẫn đó khỏi tính năng bảo vệ thời gian thực của phần mềm diệt virus.
- Dọn dẹp trước: Luôn gọi
.destroy()hoặc.close()trên các stream trước khi thực hiện unlink (xóa).

