Theo dõi lỗi Cloudflare Workers: những lỗi vẫn trả về 200
Mình deploy một Worker hỏng theo năm kiểu khác nhau rồi xem client, Cloudflare và công cụ theo dõi lỗi mỗi bên thấy gì. Hai kiểu vẫn trả 200, và một trong hai không để lại dấu vết nào.
Andrew Babo7 phút đọc
Trong bài này

Công cụ theo dõi lỗi bọc quanh một Cloudflare Worker báo những gì handler của bạn ném ra, không báo những gì handler giấu đi. Trong thí nghiệm của mình, một promise bị reject bên trong ctx.waitUntil vẫn trả 200 cho client, còn Cloudflare đánh dấu lần gọi là ok và chỉ đính kèm exception. Chính @belvador/cloudflare 0.4.0 của mình cũng không báo lỗi đó; từ bản 0.4.1 thì có.
Hai trường hợp vẫn cần code của bạn, dù dùng wrapper nào: lỗi bạn tự bắt rồi biến thành response 500, và promise không ai chờ. Phần còn lại của bài đi qua năm trường hợp mình thử và từng bên đã thấy gì.
Worker thử nghiệm#
Mình viết một Worker, mỗi route là một kiểu Worker hay hỏng:
/throw: handlerfetchném lỗi./caught-500: handler bắt lỗi rồi trảnew Response(..., { status: 500 })./wait-until: handler trảokvà giao cho Cloudflare một promise sẽ reject sau 50 ms, quactx.waitUntil./floating: cùng lỗi reject đó, nhưng trong một promise không aiawaitvà không ai đưa vàowaitUntil.scheduled(): handler cron ném lỗi.
Ngày 25/10/2026, mình chạy nó theo hai cách. Ở máy, một bản build không bọc SDK và một bản bọc @belvador/cloudflare 0.4.0 chạy trong workerd qua Miniflare 4.20260730.0, SDK gửi lỗi về một receiver trên 127.0.0.1. Sau đó mình deploy bản không bọc SDK thành một Worker tạm trên tài khoản của chính mình, gắn Tail để ghi lại góc nhìn của Cloudflare về từng lần gọi, rồi xoá Worker đi. Mỗi trường hợp chạy ba lần và lần nào cũng ra cùng kết quả.
Ai thấy lỗi nào#

| Trường hợp | Client nhận | Cloudflare (Tail) | Wrapper (@belvador/cloudflare) |
|---|---|---|---|
| Handler ném lỗi | 500, trang lỗi của Cloudflare | exception, có liệt kê lỗi | đã báo |
| Bắt lỗi, trả 500 | 500, body của bạn | ok, không có lỗi | không có gì |
waitUntil bị reject | 200 | ok, có liệt kê lỗi | 0.4.0 không có gì; từ 0.4.1 đã báo |
| Promise thả nổi | 200 | ok, không có lỗi | không có gì |
scheduled() ném lỗi | — | exception | đã báo |
Có ba điểm trong bảng này đáng nhìn kỹ.
Lỗi đã bắt chỉ còn là một mã trạng thái#
Khi code của bạn bắt lỗi rồi tự dựng response 500, không có gì bị ném ra cả. Cloudflare ghi lần gọi là ok, và wrapper không có exception nào để báo. Thứ duy nhất còn lại là con số 500 trong log request. Nếu muốn lỗi đó được theo dõi, hãy gọi captureException(error) ngay trong catch, trước khi dựng response. Tài liệu SDK Cloudflare có đúng mẫu đó.
Lỗi trong waitUntil nằm trong log của Cloudflare#
waitUntil cho phép việc tiếp tục chạy sau khi đã trả response; tài liệu context của Cloudflare cho nó tối đa 30 giây sau khi response được gửi hoặc client ngắt kết nối. Lúc promise reject thì response đã đi rồi, nên client thấy 200. Cloudflare có ghi exception vào lần gọi đó, nhưng kết quả vẫn là ok. Một dashboard hay cảnh báo chỉ đếm số lần gọi thất bại sẽ không đếm lần này.
Wrapper mình thử đầu tiên cũng không báo. Đó là lỗ hổng trong SDK của chính mình, không phải quy luật của Workers. Từ bản 0.4.1, @belvador/cloudflare báo promise waitUntil bị reject như một lỗi chưa xử lý, kèm scope của request đó, và Cloudflare vẫn nhận đúng lần reject như trước. Ngày 26/10/2026 mình chạy lại trường hợp này với 0.5.1 trong workerd ở máy: một event ở cả 3/3 lần, client vẫn nhận 200.
Nếu công cụ của bạn chưa làm vậy, hoặc bạn muốn lỗi được đánh dấu là đã xử lý kèm context của riêng bạn, hãy bắt ngay trong promise. Mẫu này được báo ở 3/3 lần chạy với cả hai phiên bản:
import { captureException, withBelvador } from "@belvador/cloudflare";export default withBelvador(() => ({}), { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { ctx.waitUntil( sendReceipt(request).catch((error) => { captureException(error); // response đã là 200; đây là bản báo lỗi duy nhất }), ); return new Response("ok"); },});Đây là những gì Tail của Cloudflare ghi cho route đó, chỉ giữ các trường quan trọng:

Promise thả nổi không để lại gì để xem#
Trong bài thử, promise không được await và không được đưa vào waitUntil không để lại exception nào ở cả ba nơi: client, Tail và wrapper. Nhìn từ bên ngoài, mình còn không biết được đoạn code sau await có chạy hay không. Cloudflare ghi rõ waitUntil là cách để việc tiếp tục chạy sau response, nên hãy coi mọi promise thả nổi khác là việc có thể âm thầm không xảy ra. Đa số linter bắt được chúng (no-floating-promises trong typescript-eslint).
Checklist cho một Worker quan trọng#
- Bọc các handler. Việc này bắt được hai trường hợp có ném lỗi:
fetchvàscheduled. Handler queue, email và tail cũng bọc giống vậy. - Báo những gì bạn tự bắt. Mọi
catchcó trả response lỗi nên gọicaptureExceptiontrước. - Đảm bảo mọi lần
waitUntilbị reject đều được báo: bởi wrapper (Belvador từ 0.4.1) hoặc bởi một.catch()của bạn. Hãy coi nó như một job chạy nền, vì bản chất nó là vậy. - Cấm promise thả nổi.
awaitchúng hoặc đưa vàowaitUntil. - Giữ log của Cloudflare luôn bật. Workers Logs gồm log lần gọi, log tự ghi, lỗi và exception chưa bắt, và Cloudflare gợi ý xuất qua OpenTelemetry, Logpush hoặc Tail Workers để gửi đi nơi khác. Đó là nhân chứng thứ hai khi wrapper bỏ sót.
- Theo dõi giới hạn thời gian riêng. Worker hết thời gian CPU sẽ hỏng với lỗi 1102 của Cloudflare; Worker chạy cron có giới hạn 15 phút thời gian thực (giới hạn). Cả hai đều không phải exception trong code của bạn, nên hãy theo dõi chính lần chạy, như trong bài cron job chạy mãi không xong.
Belvador giúp ở đâu#
@belvador/cloudflare bọc các handler của Worker bạn export, cho mỗi lần gọi một scope riêng, báo những gì chúng ném ra và, từ 0.4.1, cả promise waitUntil bị reject, rồi ném lại để Cloudflare vẫn thấy đúng lỗi đó. Response của bạn được trả nguyên vẹn, và việc gửi lỗi diễn ra sau response nên không làm người dùng phải chờ. SDK không thấy được lỗi mà code của bạn tự bắt rồi biến thành response, cũng không thấy promise không ai chờ. Trang Cloudflare Workers cho thấy phần này đi cùng trace và cron monitor ra sao, còn hướng dẫn việc chạy nền nói về những job vẫn chạy sau khi request đã kết thúc.
Đọc tiếp#
- Giám sát AI agent chạy trên serverless, bài tổng quan mà bài này thuộc về.
- Cron job chạy mãi không xong, cho Worker chạy theo lịch và những job cứ thế dừng lại.
- Lỗi đi thẳng qua error boundary của React, cùng kiểu khoảng trống nhưng ở phía trình duyệt.
Thuộc bài hướng dẫn: Giám sát AI agent chạy trên serverless function
Bài liên quan
Mọi bài viết
Hướng dẫn
Giám sát AI agent chạy trên serverless function
Một lần chạy agent trên serverless là một chuỗi lần gọi ngắn, mỗi lần có giới hạn thời gian riêng. Bài này chỉ ra cần theo dõi gì ở từng mắt xích, kèm ba thí nghiệm nhỏ cho thấy tín hiệu bị mất ở đâu.
Andrew Babo8 phút đọc

Kỹ thuật
Job thử lại, lỗi bị báo trùng: mỗi lần thử gửi gì cho công cụ theo dõi lỗi
Mình cho một job thanh toán hỏng rồi thử lại bằng SDK Node của Sentry và đếm từng event. Mười job hỏng gửi 30 event, job đã hồi phục vẫn gửi 2. Dedupe không cứu được.
Andrew Babo8 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.