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
Trong bài này

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ỗi | Ai thấy | Người dùng thấy gì |
|---|---|---|
| Render, bên trong boundary | boundary, cộng onCaughtError nếu có đặt | giao diện dự phòng |
| Render, không có boundary bên trên | mặc định là window error; chỉ onUncaughtError khi đã đặt | trang trắng |
Handler onClick | window error | không có gì thay đổi |
Callback của setTimeout | window error | không có gì thay đổi |
| Promise được await trong handler | window unhandledrejection | không có gì thay đổi |
startTransition độc lập, async hoặc sync | window error | không có gì thay đổi |
Action của useTransition | boundary | giao 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.

| Chỗ hỏng | Không có SDK | Có @sentry/node 11 |
|---|---|---|
Promise.reject() không có handler | process thoát | được báo, in cảnh báo, process vẫn chạy |
Ném lỗi trong setTimeout | process 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 await | process 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#

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ặccatch (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
asynckhông cótry. throw "message"hoặcthrow { ... }thay vìthrow new Error(...): không có stack trace.- Ném lỗi mới trong
catchmà bỏ lỗi gốc. Truyền lỗi gốc quacause, 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#
Trong chủ đề này
- Lỗi loader trong TanStack Start: vì sao stack lúc SSR lại chỉ vào trang HTML
Andrew Babo6 phút đọc
- Error boundary của React và lỗi async: thí nghiệm bắt được gì
Andrew Babo5 phút đọc
- Unhandled promise rejection và lỗi đã bắt: công cụ theo dõi lỗi nhận được gì
Andrew Babo6 phút đọc
- Source map trên production với Vite: mỗi cấu hình thực sự đưa gì vào bản build
Andrew Babo4 phút đọc
- Báo lỗi trùng lặp: vì sao một lỗi đến hai lần, còn 20 lỗi chỉ đến một
Andrew Babo6 phút đọc
Bài liên quan
Mọi bài viết
Kỹ thuật
Lỗi loader trong TanStack Start: vì sao stack lúc SSR lại chỉ vào trang HTML
Một route loader ném lỗi lúc render trên server sẽ đến trình duyệt chỉ còn mỗi message. Mình lần theo một lỗi qua ba cách cấu hình và đếm công cụ theo dõi lỗi nhận được gì mỗi lần.
Andrew Babo6 phút đọc

Hướng dẫn
Error boundary của React và lỗi async: thí nghiệm bắt được gì
Error boundary bắt ít lỗi hơn nhiều người nghĩ. Mình ném lỗi ở tám chỗ trong bản build production React 19 và ghi lại boundary, root callback và window mỗi bên thấy gì.
Andrew Babo5 phút đọc
Đọ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.