Ngăn chặn Express.js bị treo: Cách khắc phục 'Error [ERR_HTTP_HEADERS_SENT]'

intermediate💚 Node.js2026-04-23| Node.js (tất cả phiên bản), Express.js, Linux/macOS/Windows

Error Message

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
#nodejs#express#http#gỡ lỗi#backend

Thông báo lỗi

Terminal của bạn có lẽ đang hiển thị một stack trace trông giống như thế này:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at new NodeError (node:internal/errors:371:5)
    at ServerResponse.setHeader (node:_http_outgoing:576:11)
    at ServerResponse.header (/project/node_modules/express/lib/response.js:771:10)
    at ServerResponse.send (/project/node_modules/express/lib/response.js:170:12)

Nguyên nhân gốc rễ: Một yêu cầu, Một phản hồi

Hãy coi một yêu cầu (request) HTTP giống như một chiếc vé sử dụng một lần. Một khi bạn đã sử dụng nó để "mua" một phản hồi (response), nó sẽ không còn giá trị nữa. Trong giao thức HTTP/1.1, mối quan hệ giữa yêu cầu và phản hồi là nghiêm ngặt 1:1. Ngay khi bạn gọi res.send(), res.json(), hoặc res.end(), Express sẽ khóa các header và gửi chúng tới trình duyệt.

Quan trọng là, việc gọi một phương thức phản hồi không làm dừng hàm của bạn. JavaScript vẫn tiếp tục chạy từng dòng mã sau res.send() cho đến khi gặp lệnh return hoặc kết thúc khối mã. Nếu phần mã còn lại đó cố gắng gửi một phản hồi khác, Node.js sẽ ném ra lỗi này để ngăn chặn việc vi phạm giao thức.

Các tình huống phổ biến và cách khắc phục

1. Bẫy "Thiếu Return"

Đây chắc chắn là sai lầm phổ biến nhất. Các lập trình viên thường quên rằng res.send() không phải là một câu lệnh return. Nếu logic kiểm tra (validation) của bạn thất bại, mã có thể gửi một lỗi nhưng sau đó vẫn tiếp tục thực hiện logic thành công phía dưới.

Mã bị lỗi:

app.post('/login', (req, res) => {
  const { username } = req.body;

  if (!username) {
    res.status(400).json({ error: 'Missing username' });
    // Hàm vẫn tiếp tục chạy! 
  }

  // Điều này gây ra lỗi vì các header đã được gửi ở trên
  res.status(200).json({ message: 'Welcome!' }); 
});

Cách khắc phục:

Luôn thêm return trước các lệnh gọi phản hồi. Đây là một thói quen đơn giản giúp tiết kiệm hàng giờ gỡ lỗi.

app.post('/login', (req, res) => {
  const { username } = req.body;

  if (!username) {
    return res.status(400).json({ error: 'Username is required' });
  }

  return res.status(200).json({ message: 'Success' });
});

2. Phản hồi nằm trong vòng lặp

Lặp qua một danh sách và gửi phản hồi bên trong vòng lặp là một "công thức" dẫn đến thảm họa. Mục đầu tiên sẽ gửi thành công. Mục thứ hai sẽ khiến tiến trình của bạn bị sập ngay lập tức.

Mã bị lỗi:

app.get('/search', (req, res) => {
  const items = [1, 2, 3];
  items.forEach(item => {
    if (item === 2) {
      res.send('Found it!'); // Hoạt động với item 2, nhưng còn item 3 thì sao?
    }
  });
});

Cách khắc phục:

Hãy xử lý toàn bộ logic của bạn trước khi chạm vào đối tượng res. Tìm dữ liệu trước, sau đó mới gửi nó đi một lần duy nhất.

app.get('/search', (req, res) => {
  const items = [1, 2, 3];
  const match = items.find(i => i === 2);
  
  if (match) {
    return res.send('Found it!');
  }
  return res.status(404).send('Not Found');
});

3. Chồng chéo Callback bất đồng bộ

Các truy vấn cơ sở dữ liệu hoặc lệnh gọi API thường sử dụng callback. Nếu việc xử lý lỗi của bạn không dừng quá trình thực thi, bạn có thể vô tình kích hoạt cả phản hồi lỗi và phản hồi thành công.

Mã bị lỗi:

app.get('/profile', (req, res) => {
  User.findById(id, (err, user) => {
    if (err) {
       res.status(500).send('Database failure');
       // Không có return ở đây nghĩa là mã vẫn tiếp tục chạy...
    }
    res.json(user); // Bị sập ở đây nếu có lỗi xảy ra!
  });
});

Cách khắc phục:

Hãy hiện đại hóa mã của bạn với async/await. Nó làm cho luồng thực thi dễ đọc và kiểm soát hơn nhiều với các khối try/catch.

app.get('/profile', async (req, res) => {
  try {
    const user = await User.findById(id);
    return res.json(user);
  } catch (err) {
    return res.status(500).send('Database failure');
  }
});

Chiến lược gỡ lỗi từng bước

  • Xác định vị trí gọi đúp: Kiểm tra stack trace. Nó thường chỉ vào lệnh gọi res.send() thứ hai bị lỗi, chứ không phải lệnh đầu tiên đã thành công.
  • Kiểm tra các lệnh return: Tìm kiếm res. trong tệp của bạn và đảm bảo mọi nhánh logic đều kết thúc bằng return hoặc else.
  • Kiểm tra Middleware: Đảm bảo middleware tùy chỉnh của bạn không gọi next() sau khi đã gửi phản hồi. Đó là nguồn cơn phổ biến của các lỗi "ẩn".
  • Theo dõi Logs: Sử dụng một công cụ như morgan để xem chính xác những phản hồi nào đang được gửi trước khi sự cố xảy ra.

Mẹo chuyên nghiệp để viết mã sạch

  • Áp dụng mô hình "Return res": Hãy biến return res.json(...) thành cú pháp mặc định của bạn. Nó an toàn và rõ ràng hơn.
  • Sử dụng ESLint: Bật quy tắc consistent-return. Nó sẽ làm nổi bật các hàm mà bạn có thể đã bỏ lỡ một đường dẫn return.
  • Rào chắn bảo vệ: Trong các logic rất phức tạp, bạn có thể kiểm tra if (res.headersSent) return;. Tuy nhiên, hãy coi đây là giải pháp cuối cùng; nó thường là dấu hiệu cho thấy hàm của bạn đang làm quá nhiều việc và cần được cấu trúc lại (refactor).

Related Error Notes