Unhandled promise rejection và lỗi đã bắt: công cụ theo dõi lỗi nhận được gì
Mình cho 14 kiểu lỗi trình duyệt chạy qua SDK của Sentry và của chính mình, mỗi kiểu ba lần. Lỗi đã bắt mà chỉ log thì không gửi gì; reject bằng chuỗi thì không có stack.
Andrew Babo6 phút đọc
Trong bài này

Trong bài thử của mình, lỗi throw không ai bắt và promise rejection không ai xử lý đều đến được công cụ theo dõi lỗi, trừ request bị huỷ mà Belvador bỏ qua. Còn lỗi nào code đã bắt thì không, kể cả khi đã đưa vào console.error, trừ khi chính code đó tự báo. Và một rejection bằng thứ không phải Error đến nơi mà không có stack.
Ngày 26/10/2026, mình kiểm chuyện này với 14 kiểu lỗi, mỗi kiểu trên một trang mới trong Chromium 141, ba lần mỗi kiểu. Công cụ là SDK browser của Sentry để mặc định (@sentry/browser 10.76.2) và @belvador/browser 0.5.1 của chính mình, vốn được xây trên nó. Lần chạy nào cũng ra cùng một con số.
14 kiểu lỗi#
Mỗi kiểu chỉ vài dòng, giống những gì hay gặp trong code thật:
async function loadInvoice(): Promise<never> { throw new Error("invoice load failed");}void loadInvoice(); // gọi mà không awaitloadInvoice().catch((e) => console.error(e)); // bắt rồi logloadInvoice().catch((e) => captureException(e)); // bắt rồi báovoid Promise.reject("invoice load failed"); // reject bằng một chuỗi
| Kiểu lỗi | Event | Đến nơi bằng | Frame stack |
|---|---|---|---|
Throw trong setTimeout | 1 | lỗi không ai bắt | 1–2 |
| Throw trong click listener | 1 | lỗi không ai bắt | 2–4 |
Promise.reject(new Error(...)), không ai nghe | 1 | unhandled rejection | 6 |
Gọi hàm async mà không await | 1 | unhandled rejection | 7 |
fetch tới server không kết nối được, không await | 1 | unhandled rejection, TypeError | 6–7 |
| Rejection được xử lý sau 100 ms | 1 | unhandled rejection | 6 |
Bắt rồi console.error(e) | 0 | không gửi | không có |
| Bắt rồi bỏ qua | 0 | không gửi | không có |
Bắt rồi captureException(e) | 1 | được báo, handled: true | 7 |
| Reject bằng chuỗi | 1 | UnhandledRejection | 0 |
throw "một chuỗi" trong timer | 1 | lỗi lấy chuỗi làm message | 0–1 |
| Reject bằng object thường | 1 | UnhandledRejection | 0 |
| Reject không truyền gì | 1 | UnhandledRejection | 0 |
fetch bị huỷ bằng AbortController | Sentry 1, Belvador 0 | AbortError không ai xử lý | 2 |
Chỗ hai SDK khác nhau thì khoảng số ghi cả hai. Những dòng đáng chú ý nhất là những dòng có số 0.
Đã bắt là không ai thấy#
Công cụ theo dõi lỗi nghe hai sự kiện của trình duyệt: error cho lỗi throw không ai bắt, và unhandledrejection, được gửi khi “a JavaScript Promise that has no rejection handler is rejected”, tức một promise bị reject mà không có handler nào. Một handler catch chính là rejection handler. Có nó rồi thì trình duyệt không còn gì để báo, và công cụ theo dõi lỗi cũng vậy.
console.error(e) không thay đổi điều đó. Nó ghi vào console trên trình duyệt của người dùng, nơi bạn sẽ không bao giờ đọc. Để mặc định, không SDK nào biến lệnh console thành event. Sentry có một integration phải tự bật cho việc đó; tuỳ chọn console của Belvador thì chỉ ghi breadcrumb.
Một khối try/catch bọc lệnh fetch, kèm console.error và một toast, là kiểu code quen thuộc. Trong bài thử, nó không gửi event nào. Nếu người dùng thấy được là có gì đó hỏng, hãy báo nó:
try { await saveInvoice(draft);} catch (e) { captureException(e); toast.error("Không lưu được hoá đơn");}Xử lý muộn vẫn bị tính là chưa xử lý#
Một promise bị reject, rồi 100 ms sau mới gắn .catch. Lúc đó trình duyệt đã bắn unhandledrejection, nên công cụ báo nó là chưa xử lý, handled: false, ở cả ba lần chạy.
Trình duyệt có báo lại sau đó. MDN mô tả rejectionhandled, được gửi “when a handler is attached to the promise after its rejection had caused an unhandledrejection event”, tức khi handler được gắn vào sau khi rejection đã gây ra unhandledrejection. Event mình đã nhận thì vẫn giữ nguyên, vẫn là handled: false. Nếu công cụ của bạn hiện những rejection mà code thật ra có xử lý, hãy tìm các promise được tạo sớm rồi mới xử lý sau, chẳng hạn một request bắt đầu trước khi dialog mở ra. Gắn handler trước lúc promise bị reject thì sẽ không có báo cáo nào.
Reject bằng Error, đừng bằng chuỗi#
Khi code reject bằng thứ không phải Error, không có stack nào để gửi, vì chỉ object Error mới mang stack: chuỗi, object thường và undefined đều đến nơi với 0 frame. Chuỗi bị throw trong timer cũng không khá hơn mấy, Sentry gửi 0 frame, Belvador gửi 1 frame là dòng đã throw. Thay cho stack, công cụ gửi một câu:
UnhandledRejection: Non-Error promise rejection captured with value: invoice load failedUnhandledRejection: Object captured as promise rejection with keys: code, detail
Message còn; lỗi đến từ đâu thì mất. Các trường của object thường được gửi kèm trong phần extra của event, điều này đáng để ý nếu object đó chứa dữ liệu người dùng. Cách sửa rất nhỏ: reject(new Error("invoice load failed", { cause: original })), còn với thư viện bạn không sửa được thì bọc lại ở chỗ bạn gọi nó.
Nên làm gì#
- Tìm các khối
catchchỉ ghi log. Báo những lỗi mà người dùng sẽ nhận ra. - Mọi
throwvàrejectđều nên làError. Luật lint giúp được:prefer-promise-reject-errorscho rejection,@typescript-eslint/only-throw-errorcho giá trị bị throw. - Await hoặc xử lý promise ngay chỗ tạo ra nó, để handler gắn muộn không hiện thành lỗi chưa xử lý.
- Quyết định xử lý request bị huỷ thế nào. Một
AbortErrordo người dùng rời trang không phải bug; Belvador mặc định bỏ qua, Sentry thuần thì gửi. - Thử một lần ở chế độ production: gây một lỗi đã bắt và một lỗi không ai bắt, rồi xem cái nào đến nơi.
Belvador giúp ở đâu#
@belvador/browser báo lỗi không ai bắt và unhandled rejection ngay khi init chạy, bỏ qua request bị huỷ trừ khi bạn bật captureAborted, và để tắt việc bắt console. Hướng dẫn SDK trình duyệt liệt kê những gì nó báo, còn tuỳ chọn SDK nói về captureAborted và breadcrumb. Với app React, trang React có đầy đủ cách cài. Stack chỉ hữu ích khi trỏ được về source của bạn, và đó là điều bài source map trên production kiểm tra.
Đọ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.