Sửa lỗi 'ERR_STREAM_DESTROYED: Cannot call write after a stream was destroyed' trong Node.js

intermediate💚 Node.js2026-05-10| Node.js 10+, mọi nền tảng (Linux, macOS, Windows)

Error Message

Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
#nodejs#stream#writable-stream#pipe#destroy

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 curl rồi nhấn Ctrl+C khi đang stream
  • Kiểm tra log. ERR_STREAM_DESTROYED khô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 close trên HTTP response để phát hiện sớm khi client ngắt kết nối
  • Luôn kiểm tra stream.destroyed hoặc res.writableEnded trước mọi lần ghi bất đồng bộ
  • Ưu tiên dùng stream.end(); chỉ dùng stream.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 AbortController với stream.pipeline() để có pipeline có thể hủy

Related Error Notes