Lỗi
Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
Bạn đã cố ghi vào một stream đã bị hủy. Có thể client đã ngắt kết nối giữa chừng khi đang nhận response. Có thể một lệnh gọi database bất đồng bộ trả về sau 200ms — sau khi res.end() đã được gọi xong. Node.js đặt cờ nội bộ destroyed và từ chối mọi thao tác ghi tiếp theo, ném lỗi này dưới dạng unhandled exception hoặc sự kiện error trên stream.
Nguyên nhân gốc rễ
Stream trong Node.js có vòng đời nghiêm ngặt. Khi stream.destroy() được gọi — dù là do code của bạn gọi trực tiếp, hay tự động khi HTTP response kết thúc hoặc socket bị ngắt — cờ destroyed sẽ chuyển thành true. Điều này là vĩnh viễn. Bất kỳ lệnh .write() nào sau thời điểm đó đều sẽ ném lỗi này.
Các nguyên nhân thường gặp:
- Ghi vào HTTP response sau khi
res.end()đã được gọi hoặc client ngắt kết nối - Một callback bất đồng bộ (truy vấn database, gọi API ngoài) được giải quyết sau khi stream đã đóng
- Một readable stream được pipe đẩy dữ liệu sau khi writable đích đã bị hủy
- Tái sử dụng một stream object sau khi nó đã bị hủy
Cách sửa 1 — Kiểm tra destroyed trước khi ghi
Cách sửa đơn giản nhất: kiểm tra cờ trước khi ghi.
function safeWrite(stream, chunk) {
if (stream.destroyed) {
return; // bỏ qua im lặng — stream đã bị hủy
}
stream.write(chunk);
}
Với HTTP response, có hai cờ cần chú ý:
app.get('/data', async (req, res) => {
const data = await fetchSomething();
if (res.destroyed || res.writableEnded) {
return; // client ngắt kết nối hoặc res.end() đã được gọi
}
res.json(data);
});
res.writableEnded trở thành true ngay khi res.end() được gọi. res.destroyed trở thành true khi socket bên dưới đã mất. Cả hai trường hợp đều không thể phục hồi — hãy kiểm tra cả hai.
Cách sửa 2 — Xử lý sự kiện close để hủy các tác vụ bất đồng bộ
Bạn có route đang stream các hàng dữ liệu từ database? Nếu client ngắt kết nối sau khi nhận được 50 trong số 5.000 hàng, lỗi này sẽ xảy ra nếu vòng lặp của bạn tiếp tục ghi. Đặt cờ hủy tại sự kiện close và thoát sớm.
app.get('/stream-data', (req, res) => {
let cancelled = false;
res.on('close', () => {
cancelled = true; // client ngắt kết nối
});
async function streamRows() {
for await (const row of getDatabaseRows()) {
if (cancelled) break;
res.write(JSON.stringify(row) + '\n');
}
if (!cancelled) res.end();
}
streamRows().catch(err => {
if (!cancelled) res.destroy(err);
});
});
Cách sửa 3 — Dùng AbortController với Pipeline
Đang pipe stream? Truyền một AbortSignal vào stream.pipeline() để khi ngắt kết nối, toàn bộ chuỗi được hủy bỏ gọn gàng — không còn các lần ghi dở dang.
const { pipeline } = require('stream/promises');
// AbortController được tích hợp sẵn từ Node.js 16+
const ac = new AbortController();
req.on('close', () => ac.abort()); // client ngắt kết nối → hủy
try {
await pipeline(sourceStream, transformStream, res, { signal: ac.signal });
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Pipeline failed:', err);
}
}
stream.pipeline() tự động hủy mọi stream trong chuỗi khi gặp lỗi hoặc khi bị hủy. Không cần dọn dẹp thủ công.
Cách sửa 4 — Bắt sự kiện Error trên Stream
Đôi khi bạn không thể kiểm soát mọi đường dẫn ghi. Tối thiểu, hãy gắn một error handler để lỗi không làm crash toàn bộ tiến trình của bạn.
const writable = getWritableStream();
writable.on('error', (err) => {
if (err.code === 'ERR_STREAM_DESTROYED') {
// expected — stream đóng trước khi ghi xong toàn bộ dữ liệu
return;
}
console.error('Unexpected stream error:', err);
});
Đây là mạng lưới an toàn. Nó không ngăn lỗi xảy ra — chỉ ngăn lỗi làm sập server của bạn.
Cách sửa 5 — Đừng hủy Stream khi đang ghi dở
Nếu bạn kiểm soát thời điểm đóng stream, hãy để nó xả hết dữ liệu trước.
// Sai — hủy ngay lập tức, các lần ghi đang chờ sẽ thất bại
stream.write(largeChunk);
stream.destroy();
// Đúng — end() xả hết các lần ghi đang chờ rồi mới đóng
stream.write(largeChunk);
stream.end(); // chờ ghi xong rồi đóng một cách duyên dáng
Dùng stream.end() để đóng stream sạch sẽ. Giữ stream.destroy() cho các trường hợp lỗi khi bạn cần đóng cưỡng bức mà không cần xả dữ liệu — như hủy một lần upload lớn bị lỗi.
Xác minh
Sau khi sửa, hãy giả lập lỗi để xác nhận đã hết:
- Khởi động server và gọi một streaming endpoint. Ngắt kết nối giữa chừng — hủy fetch trên trình duyệt, hoặc chạy
curlrồi nhấn Ctrl+C khi đang stream - Kiểm tra log.
ERR_STREAM_DESTROYEDkhông còn xuất hiện nữa - Với CI, thêm một test hủy stream giữa pipeline và khẳng định không có unhandled error:
it('does not throw when stream is destroyed mid-write', (done) => {
const writable = new PassThrough();
writable.on('error', done); // fail test on unexpected error
writable.write('first chunk');
writable.destroy();
// safeWrite should swallow ERR_STREAM_DESTROYED silently
expect(() => safeWrite(writable, 'second chunk')).not.toThrow();
done();
});
Phòng ngừa
- Ưu tiên dùng
stream.pipeline()hoặc.pipe()thay vì vòng lặp ghi thủ công — việc dọn dẹp sẽ tự động - Lắng nghe sự kiện
closetrên HTTP response để phát hiện sớm khi client ngắt kết nối - Luôn kiểm tra
stream.destroyedhoặcres.writableEndedtrước mọi lần ghi bất đồng bộ - Ưu tiên dùng
stream.end(); chỉ dùngstream.destroy()khi cần đóng cưỡng bức mà không xả dữ liệu - Trên Node.js 16+, kết nối
AbortControllervớistream.pipeline()để có pipeline có thể hủy

