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
Trong bài thử, báo một lỗi rồi throw lại không làm nó nhân đôi. Báo rồi throw một lỗi bọc thì có. Sáu kiểu code, đếm bằng SDK browser của Sentry và SDK của chính mình.
Andrew Babo6 phút đọc
Trong bài này

Báo một lỗi rồi throw lại không làm nó bị gửi hai lần. Bọc nó trong một lỗi mới thì có. Và hai mươi lần throw giống hệt nhau liên tiếp có thể đến nơi thành một event, trong khi hai lỗi thay phiên nhau lại đến đủ hai mươi lần.
Đó là số đếm từ một bài thử mình chạy ngày 26/10/2026: sáu kiểu báo lỗi, mỗi kiểu trên một trang mới trong Chromium 141, ba lần mỗi kiểu, với SDK browser của Sentry để mặc định (@sentry/browser 10.76.2), cùng SDK đó nhưng tắt integration Dedupe, và @belvador/browser 0.5.1 của chính mình. Lần chạy nào cũng ra cùng con số.
Mình đã đếm gì#

| Kiểu code | Sentry mặc định | Sentry tắt Dedupe | Belvador |
|---|---|---|---|
captureException(e), rồi throw e | 1 | 1 | 1 |
captureException(e) hai lần | 1 | 1 | 1 |
captureException(e), rồi throw new Error("checkout failed", { cause: e }) | 2 | 2 | 2 |
captureException(e) trong .catch, rồi throw lại | 1 | 1 | 1 |
| Cùng một lỗi 20 lần liên tiếp (mỗi lần một Error mới, giống hệt nhau) | 1 | 20 | 1 |
| Hai lỗi thay phiên nhau, 20 lần throw | 20 | 20 | 20 |
Cùng một object thì chỉ báo một lần#
Báo lỗi rồi throw lại là kiểu code rất hay gặp: bạn muốn có event kèm ngữ cảnh của mình, và muốn chỗ gọi vẫn lỗi như trước. Nó không nhân đôi gì cả, kể cả khi đã tắt Dedupe.
Lý do nằm trong phần lõi của SDK. Mỗi lần capture, nó gọi checkOrSetAlreadyCaught trước, hàm này đánh dấu object lỗi bằng một thuộc tính ẩn __sentry_captured__, hoặc bỏ qua nếu dấu đó đã có. Lỗi được throw lại vẫn là object cũ, nên global handler bắt được nó sẽ thấy dấu và dừng.
Cũng trong mã nguồn đó có ghi giới hạn: kiểu nguyên thuỷ (primitive) thì không đánh dấu được. Một chuỗi bị throw không có thuộc tính nào để gắn. Mình không đếm trường hợp đó ở đây, nhưng đó thêm một lý do nữa để throw object Error, bên cạnh cái stack mà nó mang theo, như bài thử về rejection đã đo.
Dấu đó cũng chỉ nằm trên object đó, trong process đó. Khi TanStack Start dựng lại lỗi của server trong trình duyệt, bản sao không có dấu, và bài thử về loader nhận hai event cho một lỗi, cho đến khi server báo cho trình duyệt biết lỗi nào nó đã gửi.
Bọc lại là thành lỗi thứ hai#
new Error("checkout failed", { cause: e }) là cách làm tốt. MDN mô tả cause là chỗ để ghi “the specific original cause of the error”, tức nguyên nhân gốc của lỗi, và SDK gửi cả hai: event của lỗi bọc có hai exception trong chuỗi, lỗi bọc và lỗi gốc.
Nhưng lỗi bọc là một object mới, không có dấu, nên nếu trước đó đã gọi captureException(e) thì lỗi gốc xuất hiện hai lần: một lần riêng, một lần làm cause. Hãy chọn một chỗ để báo: hoặc capture lỗi gốc rồi throw lại nguyên như vậy, hoặc bọc nó và để lỗi bọc là lần báo duy nhất.
// Hoặc: báo ở đây, throw lại đúng lỗi đócatch (e) { captureException(e); throw e; }// Hoặc: bọc lại, và chỉ báo lỗi bọc (ở tầng trên, hoặc để không ai bắt)catch (e) { throw new Error("checkout failed", { cause: e }); }Dedupe chỉ bỏ cái vừa đến ngay trước#
Tài liệu Sentry nói integration Dedupe “deduplicates certain events”, tức bỏ bớt một số event trùng, và “only compares stack traces and fingerprints”, tức chỉ so stack trace và fingerprint. Trên thực tế, nó so mỗi event với event ngay trước đó.

Hai mươi lần throw giống hệt nhau liên tiếp chỉ thành một event. Tắt Dedupe thì đủ hai mươi. Hai lỗi thay phiên nhau, A B A B, thì lúc nào cũng là hai mươi event, vì không event nào giống hệt event ngay trước nó. Một vòng lặp hỏng cùng một kiểu thì yên ắng; một trang có hai widget hỏng chạy theo timer thì không.
Vậy Dedupe giúp quota của bạn khỏi bị một vòng lặp liên tục phát sinh lỗi ăn hết, điều đó có ích. Nhưng nó không phải gom nhóm. Hai mươi event của một lỗi vẫn phải nằm trong một issue, và đó là việc của công cụ theo dõi lỗi ở phía server, không phải của SDK.
Danh sách kiểm tra#
- Báo mỗi lỗi ở đúng một chỗ. Capture rồi throw lại đúng object đó, hoặc bọc lại và chỉ báo lỗi bọc.
- Throw object
Errorđể SDK đánh dấu được. - Giữ Dedupe bật ở trình duyệt. Trong ca vòng lặp, nó giảm 20 event xuống 1.
- Bản trùng giữa các process, như server và trình duyệt, cần một bước bàn giao riêng.
- Khi số đếm trông lạ, hãy tái hiện kiểu code đó một lần và đếm những gì đến nơi, trước khi đụng vào cách gom nhóm.
Belvador giúp ở đâu#
@belvador/browser giữ nguyên dấu đánh dấu và Dedupe của engine, nên ở đây nó đếm giống Sentry mặc định. Trên node, Belvador gom event thành issue bằng một thuật toán có đánh version, nên hai mươi event của một lỗi là một issue kèm số đếm, và version gom nhóm mới không bao giờ gom lại issue cũ. Cách cài có trong hướng dẫn SDK trình duyệt, còn trang React dành cho app React. Khi đã hết trùng, bước tiếp theo là quyết định lỗi nào đáng cảnh báo.
Đọ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.