Bỏ qua tới nội dung

Hướng dẫn

Xử lý lỗi JavaScript trong ứng dụng do AI viết: lỗi thoát ra từ đâu

Mình cố tình gây lỗi ở tám chỗ trong một trang React 19 và năm chỗ trong một server Node.js, mỗi chỗ ba lần. Có lỗi làm sập server, có lỗi để lại một nút bấm không phản hồi, có lỗi treo request mãi.

Andrew Babo7 phút đọc

Chia sẻ
Trong bài này
Xử lý lỗi JavaScript trong ứng dụng do AI viết: lỗi thoát ra từ đâu

Lỗi JavaScript thoát khỏi ứng dụng React và Node.js qua bốn cửa: lỗi render không có boundary, lỗi trong handler hay promise đi vòng qua React, rejection không ai xử lý làm sập server Node.js, và việc nền hỏng sau khi đã trả response. Mỗi cửa cần vài dòng code riêng. Bài này cho thấy từng cửa trông ra sao và cách đóng nó.

Mình thử hết bốn cửa vào ngày 26/10/2026: tám kiểu lỗi trong bản build production React 19 chạy trên Chromium và năm kiểu trong một server Node.js 22, mỗi kiểu ba lần. Lần nào kết quả cũng giống nhau.

Chuyện này càng quan trọng khi phần lớn code do công cụ AI viết. Không phải vì code tệ hơn, mà vì bạn không tự viết các nhánh xử lý lỗi, nên có thể không biết chúng tồn tại.

Trình duyệt: tám cách một trang React có thể hỏng#

Trang thử có một error boundary bọc quanh một hàng nút, và tự lắng nghe sự kiện error và unhandledrejection của window. Mỗi nút hỏng theo một kiểu.

Chỗ ném lỗiAi thấyNgười dùng thấy gì
Render, bên trong boundaryboundary, cộng onCaughtError nếu có đặtgiao diện dự phòng
Render, không có boundary bên trênmặc định là window error; chỉ onUncaughtError khi đã đặttrang trắng
Handler onClickwindow errorkhông có gì thay đổi
Callback của setTimeoutwindow errorkhông có gì thay đổi
Promise được await trong handlerwindow unhandledrejectionkhông có gì thay đổi
startTransition độc lập, async hoặc syncwindow errorkhông có gì thay đổi
Action của useTransitionboundarygiao diện dự phòng

Có hai dòng đáng đọc lại. Lỗi render không có boundary bên trên xoá sạch giao diện, và khi app tự truyền onUncaughtError vào createRoot, window không còn nghe thấy lỗi đó nữa. Còn với phần lớn lỗi, React giữ nguyên giao diện: người dùng bấm nút và chẳng có gì xảy ra. Đó là kiểu lỗi không bao giờ có ai báo cho bạn.

Bài error boundary của React và lỗi async có toàn bộ thí nghiệm và cách cài cho từng trường hợp.

Server: năm cách một process Node.js có thể hỏng#

Từ phiên bản 15, Node.js đổi mặc định cho rejection không ai xử lý thành throw: “Changed default mode to throw. Previously, a warning was emitted.” Mình đo xem điều đó nghĩa là gì với một server đang xử lý dở một request chậm.

Cái gì thấy được từng kiểu lỗi trong 13 kiểu của hai thí nghiệm React và Node.js
Thí nghiệm 26/10/2026 · React 19.2 trên Chromium, Node.js 22 · mỗi kiểu 3 lần
Chỗ hỏngKhông có SDKCó @sentry/node 11
Promise.reject() không có handlerprocess thoátđược báo, in cảnh báo, process vẫn chạy
Ném lỗi trong setTimeoutprocess thoátđược báo, process vẫn thoát
try/catch bọc ngoài setTimeout đóprocess thoát, catch không bao giờ chạyđược báo, process vẫn thoát
Handler HTTP async ném lỗi sau awaitprocess thoát, request đang chạy cũng chết theođược báo; request đó không bao giờ nhận được response
Promise chạy nền bị reject sau res.end("ok")client nhận 200, rồi process thoátđược báo; mọi request đều được trả lời

Không có SDK, chỉ một rejection không ai await trong một handler đã kéo sập cả server, kèm theo một request chẳng làm gì sai. SDK thay đổi điều đó: nó đăng ký listener unhandledRejection riêng, báo lỗi và giữ process sống.

Process còn sống không có nghĩa là request được trả lời. Có SDK, request có handler ném lỗi không bao giờ nhận được response, và client của mình bỏ cuộc sau hai giây. Người dùng sẽ thấy vòng quay tải mãi. Chỉ một try/catch trả về 500, hoặc error handler của framework, mới đóng được request đó.

Dòng try/catch bọc ngoài setTimeout là cái bẫy lâu đời nhất của JavaScript. MDN viết rõ: lỗi ném ra từ một callback chạy sau “are not catchable by the caller”, tức là nơi gọi không bắt được.

Bốn cửa và cách đóng từng cửa#

Bốn cửa lỗi JavaScript thoát ra, và cái gì đóng được từng cửa
Từ hai thí nghiệm ngày 26/10/2026

1. Lỗi render: boundary và root callback#

Bọc từng phần dễ hỏng của trang bằng error boundary, để một widget hỏng chỉ hiện giao diện dự phòng chứ không làm trắng cả trang. Với React 19, truyền hàm báo lỗi vào cả ba tuỳ chọn của createRoot: onCaughtError, onUncaughtError và onRecoverableError. Lỗi đã được boundary bắt sẽ không bao giờ tới window, nên công cụ chỉ nghe ở đó sẽ không thấy chúng.

2. Handler, timer và promise: lắng nghe trên window#

Những lỗi này đi vòng hẳn qua React. Công cụ theo dõi lỗi trên trình duyệt lắng nghe window error và unhandledrejection; theo MDN, sự kiện sau xảy ra “when a JavaScript Promise that has no rejection handler is rejected”. Hãy chắc rằng nó khởi động trước code của app, và không có gì thay thế window.onerror sau đó.

3. Handler trên server: bắt lỗi, trả lời, báo lỗi#

Trong mọi request handler có await, hãy bắt lỗi, trả về 500 và báo lỗi. Express, Fastify và các framework khác có error handler cho việc này; dùng nó, và kiểm tra là nó có báo lỗi. Đừng thêm một dòng process.on("unhandledRejection", () => {}) trống để hết sập: nó giấu mọi bug về sau và để request treo.

4. Việc chạy sau response: báo lỗi từ bên trong#

Việc chạy nền, waitUntil trên Cloudflare, hay một promise void sau res.end() hỏng khi chẳng còn ai chờ. Gắn .catch() để báo lỗi cho nó. Bài giám sát agent chạy trên serverless gặp đúng khoảng trống này trên edge: một promise waitUntil bị reject mà client vẫn nhận 200.

Cần tìm gì trong code bạn không tự viết#

Những mẫu sau đáng tìm nhanh trong bất kỳ codebase nào, nhất là code mà công cụ AI viết nhanh hơn tốc độ bạn kịp đọc:

  • catch {} hoặc catch (e) {} trống: lỗi biến mất.
  • catch (e) { console.log(e) } trên server: lỗi rơi vào log không ai đọc.
  • .then( mà không có .catch( trên cùng chuỗi, và void someAsyncCall().
  • Request handler async không có try.
  • throw "message" hoặc throw { ... } thay vì throw new Error(...): không có stack trace.
  • Ném lỗi mới trong catch mà bỏ lỗi gốc. Truyền lỗi gốc qua cause, thứ giữ lại “the specific original cause of the error”.

Khi lỗi đã được báo, hãy làm stack trace đọc được: một frame đã minify như app.mjs:6:29 gần như chẳng nói gì. Source map giải quyết chuyện đó, và tài liệu source map chỉ cách tải map lên mà không công khai mã nguồn.

Belvador giúp ở đâu#

Gói trình duyệt và React của Belvador lắng nghe lỗi và rejection trên window, có sẵn error boundary, và cung cấp reactErrorHandler() cho ba tuỳ chọn root của React 19. Hướng dẫn SDK trình duyệt và hướng dẫn SDK React có phần cài đặt. Trên Node.js, Belvador dùng SDK Sentry chính thức với DSN của Belvador, chính SDK mình đã thử ở đây; hướng dẫn Node.js có chi tiết. Trang React và trang Node.js gom toàn bộ phần cài đặt vào một chỗ.

Đọc tiếp#

Andrew Babo

Nhà sáng lập

Trong chủ đề này

Mọi bài viết

Đọc tiếp trong tài liệu

Xem lỗi production của chính bạn như thế này

Belvador chạy trên máy chủ Hetzner của chính bạn: lỗi, cron, uptime và trace — dữ liệu vẫn là của bạn.