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

Error boundary của React bắt lỗi ném ra trong lúc render, và lỗi ném ra trong action của useTransition. Chỉ vậy thôi. Lỗi trong handler, timer và promise đi vòng qua nó và chỉ tới sự kiện của window, còn lỗi boundary đã bắt thì không bao giờ tới window.
Vì vậy công cụ theo dõi lỗi chỉ nghe trên window sẽ bỏ sót mọi lỗi mà boundary bắt được. Công cụ chỉ dựa vào boundary thì bỏ sót mọi lỗi trong handler. Bạn cần cả hai, cộng với các root callback của React 19.
Mình thử tám trường hợp vào ngày 26/10/2026 trên bản build production React 19.2 chạy trong Chromium. Mỗi trường hợp chạy ba lần với tuỳ chọn root mặc định của React và ba lần khi đặt đủ ba root callback. Lần nào kết quả cũng giống nhau.
Trang thử#
Trang có một class error boundary bọc quanh một hàng nút. Mỗi nút hỏng theo một kiểu. Trang cũng tự lắng nghe window error và unhandledrejection, để mình thấy được công cụ theo dõi lỗi trên trình duyệt sẽ nhận gì.
const root = createRoot(container, { onCaughtError: (error) => note("root.onCaughtError", error), onUncaughtError: (error) => note("root.onUncaughtError", error), onRecoverableError: (error) => note("root.onRecoverableError", error),});Cấu hình thứ hai không truyền tuỳ chọn nào, nên React dùng mặc định.
Mỗi bên thấy gì#

| Chỗ ném lỗi | Boundary | Có đặt root callback | Mặc định | Trang |
|---|---|---|---|---|
| Render, bên trong boundary | bắt được | onCaughtError | không gì khác | giao diện dự phòng |
| Render, không có boundary bên trên | không | chỉ onUncaughtError | window error | trắng |
Handler onClick | không | window error | window error | không đổi |
Callback của setTimeout | không | window error | window error | không đổi |
| Promise được await trong handler | không | window unhandledrejection | như vậy | không đổi |
startTransition độc lập, async | không | window error | window error | không đổi |
startTransition độc lập, sync | không | window error | window error | không đổi |
Action của useTransition | bắt được | onCaughtError | không gì khác | giao diện dự phòng |
onRecoverableError không lần nào chạy: nó dành cho lỗi React tự phục hồi được, như lệch hydration, còn trang này chỉ render trên trình duyệt.
Ba điều bảng này cho thấy#
Boundary bắt ít hơn cái tên gợi ý#
Tài liệu React liệt kê những gì boundary không bắt, trong đó có “Event handlers” và code bất đồng bộ. Thí nghiệm khớp với danh sách đó. Năm trong tám trường hợp đi thẳng qua boundary, và cả năm lần trang vẫn y nguyên. Người dùng bấm một nút hỏng sẽ chẳng thấy gì.
Lỗi đã bắt thì window không thấy#
Khi boundary bắt được lỗi, không có gì tới window ở cả hai cấu hình. Mặc định React ghi lỗi đã bắt ra console, mà console thì không phải là báo lỗi. Nếu công cụ theo dõi lỗi chỉ móc vào window.onerror, nó không bao giờ biết tới những lỗi boundary đã xử lý.
Đó là lý do React 19 thêm hai tuỳ chọn root “to complement onRecoverableError”: onCaughtError và onUncaughtError. Hãy báo lỗi từ đó, hoặc từ componentDidCatch trong boundary của bạn.
Đặt onUncaughtError là giành luôn đường đi của window#
Với tuỳ chọn mặc định, lỗi render không có boundary bên trên tới được window error, và cả trang trắng. Khi đặt onUncaughtError, cùng lỗi đó chỉ tới callback này. Nếu bạn đặt nó để ghi log mà quên báo lỗi, bạn mất đúng loại lỗi tệ nhất, loại làm trắng trang.
Bất ngờ với transition#
Tài liệu về boundary có thêm một ngoại lệ cho quy tắc async: “Errors thrown inside the transition function are caught by error boundaries”. Trong thí nghiệm, điều đó đúng với startTransition do hook useTransition trả về. startTransition độc lập import từ react thì gửi cùng lỗi đó, async hay sync, tới window error, và boundary không hiện gì.
Mình không nói tài liệu sai; mình chỉ thử một phiên bản trên một trình duyệt. Nếu bạn dựa vào boundary để bắt lỗi của action, hãy dùng useTransition, và tự thử cấu hình của mình bằng một lỗi cố ý.
Cách cài thấy được mọi trường hợp#

- Công cụ theo dõi lỗi khởi động trước app và lắng nghe
windowerrorvàunhandledrejection. Phần này lo handler, timer, promise và transition độc lập. - Đủ ba root callback, cái nào cũng báo lỗi. Phần này lo lỗi render boundary đã bắt, lỗi render không boundary nào bắt, và lỗi phục hồi được sau hydration.
- Boundary quanh những phần dễ hỏng của trang, để một biểu đồ hỏng chỉ hiện giao diện dự phòng thay vì làm trắng tất cả.
- Thông báo rõ ràng trong handler bị lỗi. Một
try/catchhiện thông báo, kèm báo lỗi, để người dùng không phải bấm mãi một nút chết.
Belvador giúp ở đâu#
@belvador/react gồm các listener trên window của SDK trình duyệt, một ErrorBoundary báo lỗi kèm component stack, và reactErrorHandler() cho ba tuỳ chọn root của React 19. Hướng dẫn SDK React đặt cách cài cho React 18 và React 19 cạnh nhau, còn hướng dẫn SDK trình duyệt lo phần listener trên window. Trang React gom toàn bộ phần cài đặt vào một chỗ.
Đọc tiếp#
Thuộc bài hướng dẫn: Xử lý lỗi JavaScript trong ứng dụng do AI viết: lỗi thoát ra từ đâu
Bài liên quan
Mọi bài viết
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

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
Đọ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.