Cách khắc phục lỗi 'Error: write after end' trong Node.js Streams

intermediate💚 Node.js2026-04-03| Node.js (khuyên dùng v12.9.0+ cho writableEnded), xảy ra trên mọi hệ điều hành trong các thao tác stream hoặc HTTP.

Error Message

Error: write after end
#nodejs#streams#expressjs#backend#xu-ly-loi

Giải pháp trong 30 giây

Lỗi này là cách Node.js thông báo rằng bạn đang cố gắng gửi dữ liệu đến một stream đã được đóng. Nó giống như việc cố gắng gửi một bức thư sau khi bưu điện đã khóa cửa nghỉ đêm. Điều này thường xảy ra do .end() được gọi quá sớm hoặc gọi nhiều lần. Để dừng việc crash ngay lập tức, hãy kiểm tra trạng thái của stream trước khi ghi:

if (!myStream.writableEnded) {
  myStream.write(data);
}

Nếu bạn đang sử dụng .pipe(), hãy ngừng gọi .end() thủ công tại điểm đích (destination). Cơ chế pipe sẽ tự động xử lý quá trình chuyển đổi đó cho bạn.

Tại sao lỗi này xảy ra

Mọi Writable Stream trong Node.js đều tuân theo một vòng đời nghiêm ngặt. Khi bạn báo hiệu rằng không còn dữ liệu nào được gửi đến nữa bằng cách gọi .end(), stream sẽ chuyển sang trạng thái hoàn tất. Bất kỳ nỗ lực nào để sử dụng .write() hoặc gọi .end() một lần nữa sau thời điểm này đều sẽ kích hoạt exception. Trong môi trường high-concurrency—ví dụ, xử lý 1.000 request mỗi giây—những lỗi này thường chỉ ra các lỗ hổng logic trong mã asynchronous.

Những trường hợp thường gây ra lỗi:

  • Double Response trong Express: Bạn có thể kích hoạt res.json() nhưng để mã tiếp tục thực thi đến một lệnh res.send() khác ở phía sau trong cùng một hàm.
  • The Async Race: Một truy vấn database hoặc một lời gọi API bên ngoài trả về kết quả sau khi client đã timeout hoặc request đã bị đóng.
  • Can thiệp thủ công vào Pipe: Bạn đóng một file stream một cách thủ công trong khi fs.createReadStream().pipe(dest) vẫn đang cố gắng đẩy các chunk dữ liệu.
  • Vòng lặp trong trình xử lý lỗi (Error Handler): Một lỗi xảy ra, trình xử lý của bạn đóng stream, nhưng logic ban đầu vẫn cố gắng hoàn thành thao tác ghi hiện tại.

Các cách khắc phục thực tế

1. Bảo vệ các thao tác ghi

Kiểm tra thuộc tính writableEnded là tuyến phòng thủ đầu tiên của bạn. Nó đặc biệt hữu ích khi làm việc với các stream không dự đoán trước được từ bên thứ ba hoặc các event emitter phức tạp.

function safelyWrite(stream, data) {
  // writableEnded được giới thiệu trong Node v12.9.0
  if (stream.writable && !stream.writableEnded) {
    stream.write(data);
  } else {
    console.warn("Prevented a write attempt to a closed stream");
  }
}

2. Chuyển sang sử dụng pipeline()

Phương thức source.pipe(dest) cổ điển thường tiềm ẩn nguy cơ rò rỉ bộ nhớ vì nó không hủy toàn bộ chuỗi nếu một stream thất bại. Công cụ stream.pipeline là một giải pháp thay thế an toàn hơn nhiều. Nó quản lý việc dọn dẹp cho bạn và đảm bảo rằng nếu điểm đích bị đóng, nguồn sẽ dừng ngay lập tức.

const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');

// Cách này xử lý lỗi đúng cách và ngăn chặn các thao tác ghi mồ côi
pipeline(
  fs.createReadStream('large-archive.tar'),
  zlib.createGzip(),
  fs.createWriteStream('large-archive.tar.gz'),
  (err) => {
    if (err) {
      console.error('Pipeline failed:', err);
    } else {
      console.log('Compression complete');
    }
  }
);

3. Sửa logic 'Return' trong Express

Trong Express, res.send()res.json() sẽ tự động gọi res.end(). Nếu bạn không sử dụng từ khóa return, phần còn lại của hàm vẫn sẽ chạy, dẫn đến nỗ lực ghi lần thứ hai.

// CÁCH LÀM SAI
app.get('/user/:id', (req, res) => {
  if (!req.params.id) {
    res.status(400).send('ID required');
  }
  res.send('User Data'); // CRASH: write after end nếu thiếu ID
});

// CÁCH LÀM ĐÚNG
app.get('/user/:id', (req, res) => {
  if (!req.params.id) {
    return res.status(400).send('ID required'); // 'return' thoát khỏi hàm
  }
  res.send('User Data');
});

4. Bảo vệ các Async Callback

Khi làm việc với kết quả từ database, hãy luôn kiểm tra xem client còn đang lắng nghe hay không. Nếu một truy vấn mất 5 giây nhưng người dùng tải lại trình duyệt sau 2 giây, stream sẽ bị đóng khi dữ liệu trả về.

db.users.find({ id }, (err, user) => {
  if (res.writableEnded) return; // Người dùng đã rời đi; không cố gắng gửi dữ liệu

  if (err) return res.status(500).send(err);
  res.json(user);
});

Cách xác minh bản sửa lỗi

Đừng chỉ giả định là nó đã được sửa. Hãy kiểm tra các kịch bản sau:

  • Mô phỏng việc Client hủy bỏ: Sử dụng một công cụ như curl và nhấn Ctrl+C giữa chừng request để xem server của bạn có log lỗi unhandled exception nào không.
  • Stress Testing: Chạy lệnh autocannon -c 100 -d 10 http://localhost:3000/data. Tải cao thường làm lộ ra các race conditions không xuất hiện trong môi trường phát triển cục bộ.
  • Kiểm tra trạng thái Stream: Sử dụng console.log('Finished:', stream.writableFinished) để theo dõi chính xác thời điểm vòng đời của stream kết thúc.

Tài liệu tham khảo

Related Error Notes