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

Một route loader của TanStack Start ném lỗi lúc render trên server sẽ đến trình duyệt chỉ còn message, không còn gì khác. Nếu công cụ theo dõi lỗi của bạn chỉ chạy ở trình duyệt, nó báo lỗi đã được dựng lại đó, và stack chỉ vào trang HTML thay vì loader. Nếu bạn báo thêm từ server, cùng một lỗi có thể bị đếm hai lần.
Cách sửa là báo ở server, nơi có stack thật, rồi báo cho trình duyệt biết lỗi đó đã được xử lý. Ngày 26/10/2026, mình lần theo một loader lỗi qua ba cách cấu hình và đếm từng event.
Vì sao stack biến mất#
Ở lần tải trang đầu, TanStack Start chạy các route loader trên server, render HTML và gửi kèm state của router, gồm cả lỗi. Trình duyệt nhận state đó và hiện error component của route.
Lỗi không đi qua ranh giới đó nguyên vẹn. Bộ serialize của router có một plugin dành cho lỗi, và mã nguồn của nó nói rõ giữ lại gì: “this plugin serializes only the message part of an Error”, tức chỉ serialize phần message. Comment trong code cũng nêu lý do, những lỗi có gắn function thì không serialize được, và đó là một lựa chọn hợp lý. Ở phía bên kia, nó thành new Error(message), được dựng bởi một đoạn script nằm trong trang.
Vậy là trình duyệt giữ một lỗi có message đúng, một stack mới tinh, và không còn dấu vết nào của loader.

Bài thử#
Một app TanStack Start nhỏ, build production với @tanstack/react-start 1.168.60 và @tanstack/router-core 1.171.34. Route /orders có một loader luôn ném lỗi:
function loadOrders(): { count: number } { throw new Error("orders loader failed on the " + (typeof window === "undefined" ? "server" : "browser"));}export const Route = createFileRoute("/orders")({ loader: () => loadOrders(), component: Orders });Mình mở thẳng /orders để loader chạy lúc render trên server, và điều hướng sang nó từ / để loader chạy trong trình duyệt. Mỗi kiểu ba lần, trên Chromium, dùng một collector chạy cục bộ để ghi lại mọi event: đến từ đâu, được bắt bằng cách nào, các frame trên cùng là gì. Mình dùng SDK của chính mình, @belvador/tanstack-start, với ba cách cấu hình.
Kết quả nhận được#

| Cấu hình | Số event mỗi lỗi SSR | Báo từ | Stack |
|---|---|---|---|
| Chỉ trình duyệt (SDK 0.5.0) | 1 | trình duyệt | 2 frame, đều là trang HTML /orders |
| Trình duyệt và server, không bàn giao | 2 | server, rồi trình duyệt | trùng lặp |
| Trình duyệt và server, có bàn giao (0.5.1) | 1 | server | loadOrders và loader trong bundle của server |
Chỉ trình duyệt. Lần chạy nào cũng báo lỗi đúng một lần, vậy là tốt: không mất gì. Nhưng cả hai frame đều là chính /orders, trang đã dựng lại lỗi. Người đọc issue đó biết message và URL, còn loader thì phải tự đi tìm. Khi điều hướng phía client, cùng cấu hình này cho kết quả tốt hơn, vì loader chạy trong trình duyệt và stack hiện loader trong bundle.
Cả hai phía, không bàn giao. Báo lỗi từ onError của route trên server cho stack đúng, rồi trình duyệt lại báo thêm bản đã dựng lại: hai event cho một lỗi, ở cả ba lần chạy. Một công cụ đánh dấu lỗi là "đã gửi" cũng không nhận ra bản sao, vì dấu đó nằm trên object lỗi gốc, mà object này chưa bao giờ rời server.
Cả hai phía, có bàn giao. Server báo lỗi, rồi thêm một mã hash ngắn của message vào state gửi kèm trang. Trình duyệt bỏ qua một lỗi có message khớp với hash đó. Một event cho mỗi trang lỗi, từ server, kèm các frame của loader. Với 40 request đồng thời, một nửa vào /orders và một nửa vào /, cả 20 trang lỗi đều mang hash, không trang chủ nào trong 20 trang mang hash, và collector nhận đúng 20 event từ server.
Cấu hình thế nào, với công cụ nào cũng được#
TanStack Router cho bạn hai hook, và chúng trả lời hai câu hỏi khác nhau. onError được gọi “when an error occurs during the route loading”, tức khi có lỗi trong lúc tải route; đó là chỗ để báo ở server, nơi vẫn còn object lỗi gốc. onCatch và defaultOnCatch chạy khi một error boundary bắt được lỗi, mà ở trình duyệt thì gồm cả bản sao của lỗi SSR.
- Báo lỗi loader ở server, từ
onErrorhoặc từ phần server của SDK, để stack là stack thật. - Quyết định cách trình duyệt tránh bản sao. Hoặc bỏ qua những lỗi server đã báo, như ở trên, hoặc chấp nhận hai event rồi gom chúng lại. Đừng bỏ hết mọi lỗi SSR ở trình duyệt: một app không báo lỗi từ server sẽ mất chúng.
- Đảm bảo có error component, đặt ở route hoặc qua
defaultErrorComponent, như hướng dẫn error boundary của TanStack Start. Router không gọidefaultOnCatchkhi rơi vào fallback ngoài cùng của nó. - Thử một lần trên bản build production: ném lỗi từ một loader, mở thẳng trang, rồi điều hướng sang nó, và đếm số event. Hãy thử trên đúng bản build bạn deploy, không chỉ trên dev server.
Belvador giúp ở đâu#
@belvador/tanstack-start 0.5.1 làm đúng bước bàn giao ở trên: withBelvadorRouter bọc các option của router, phần server báo lỗi loader kèm stack của server, rồi trình duyệt bỏ qua đúng lỗi đó. Không có phần server thì trình duyệt vẫn báo lỗi một lần. Mã hash không bao giờ chứa message, và dehydrate cùng hydrate của riêng bạn vẫn chạy như cũ. Hướng dẫn TanStack Start chỉ rõ ba chỗ cần cấu hình SDK, còn trang TanStack Start cho thấy phần còn lại của cách cài. App Lovable là app TanStack Start, nên bài app Lovable bị trang trắng nói về kiểu hỏng còn lại của những app này ngay lần tải đầu.
Đọ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

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.