Bạn có một dashboard đẹp: vài ô số liệu và một widget phức tạp. Widget đó gặp dữ liệu bất thường và ném lỗi khi render. Kết quả mặc định: không chỉ widget đó, mà cả màn hình sập trắng với "Rất tiếc, đã xảy ra lỗi" — vì lỗi lan ngược lên tận gốc cây component. Người dùng mất luôn cả những ô số liệu vốn chẳng liên quan. OWL có cách chặn đám cháy đó ở đúng một ô: error boundary bằng hook onError.

onError bắt lỗi từ component con

onError((err) => {...}) là một hook đăng ký trong setup(). Nó bắt lỗi phát sinh từ các component con (khi render hoặc trong lifecycle như onWillStart, onMounted). Khi bắt được, thay vì để lỗi lan lên, bạn đặt một cờ trong state và cho template hiện một giao diện thay thế (fallback):

Ảnh chụp đoạn mã OWL nền tối. Import onError từ owl. Lớp KhungAnToan kế thừa Component, trong setup tạo this.state useState với loi null, gọi onError với callback nhận err và gán this.state.loi bằng err.message kèm chú thích onError bắt lỗi từ mọi component con khi render lifecycle và chuyển sang hiện fallback. Template boundary có div t-if state.loi class alert-danger hiện cảnh báo Ô này gặp sự cố phần còn lại vẫn hoạt động, và t-else t-slot default để con render ở đây. Phần dùng bọc ThanhPhanLoi trong KhungAnToan kèm chú thích con này ném lỗi boundary bắt và ba thẻ số liệu bên ngoài vẫn render bình thường

Hình 1: KhungAnToan là một error boundary. onError bắt lỗi từ con (<ThanhPhanLoi/> đặt trong slot), đặt state.loi. Template: có lỗi thì hiện fallback (t-if="state.loi"), không thì render con (t-slot="default"). Boundary chỉ bọc phần có nguy cơ; ba ô số liệu nằm ngoài nó nên không bị ảnh hưởng.

Thấy tận mắt: một ô cháy, phần còn lại nguyên vẹn

Mình cố tình cho ThanhPhanLoi đọc thuộc tính của null khi render (this.duLieu.gia_tri với duLieu = null) — một lỗi rất hay gặp khi dữ liệu chưa kịp tải. Bọc nó trong KhungAnToan, đặt cạnh ba ô số liệu:

Ảnh chụp thật bảng điều khiển Thẻ thành viên trên Odoo 19, ba thẻ số liệu Tổng số thẻ 4, Tổng điểm 385 màu xanh, Thẻ Vàng 1 màu vàng đều hiển thị bình thường. Bên dưới có dòng Ô widget lỗi được bọc bởi KhungAnToan và một hộp cảnh báo nền hồng đỏ với biểu tượng tam giác ghi Ô này gặp sự cố phần còn lại vẫn hoạt động, dòng nhỏ Chi tiết An error occured in the owl lifecycle

Hình 2: Kết quả thật. Widget lỗi bị chặn trong hộp đỏ "Ô này gặp sự cố, phần còn lại vẫn hoạt động" — không sập gì hết. Ba ô số liệu (4, 385, 1) render bình thường vì chúng nằm ngoài boundary. So với mặc định (cả trang trắng), khác biệt là một trải nghiệm hỏng cục bộ có thể chịu được thay vì mất trắng. Console cũng ghi lại lỗi đã bắt để lập trình viên còn lần ra.

Vài điều cần biết cho đúng

  • onError chỉ bắt lỗi từ component CON khi render/lifecycle. Nó không bắt lỗi trong một Promise chưa await, một setTimeout, hay một event handler async — những lỗi đó xảy ra ngoài chu trình render, boundary không thấy. Xử lý chúng bằng try/catch tại chỗ.
  • Vẫn phải log lỗi. Bắt để UI đẹp không có nghĩa là giấu lỗi. Ghi console (hoặc gửi về hệ thống theo dõi lỗi) để còn sửa — như ví dụ trên có console.warn.
  • Đặt boundary ở đúng độ sâu. Bọc quá rộng (cả trang) thì một lỗi nhỏ vẫn ẩn cả trang; bọc quá hẹp thì viết nhiều. Bọc quanh những mảnh độc lập, dễ hỏng (widget lấy dữ liệu ngoài, biểu đồ, phần do người dùng cấu hình).
  • Odoo lõi đã dùng pattern này. Web client bọc các phần chính bằng error handler để một action hỏng không giết cả giao diện — bạn đang dùng đúng cơ chế đó cho phần của mình.

Ba ý mang về

  1. Một lỗi khi render một component có thể kéo sập cả cây giao diện. onError((err) => {...}) là error boundary: bắt lỗi từ component con, chuyển sang hiện fallback, giữ phần còn lại của UI sống.
  2. Mẫu viết: setup() gọi onError đặt state.loi; template t-if="state.loi" hiện thông báo thay thế, t-else render con (thường qua t-slot). Bọc quanh những mảnh độc lập, dễ hỏng — đừng bọc cả trang.
  3. onError chỉ bắt lỗi render/lifecycle của con, KHÔNG bắt lỗi async rời rạc (Promise chưa await, setTimeout, handler async) — dùng try/catch cho chúng. Và vẫn phải log lỗi để còn sửa.

Ta đã viết khá nhiều JS cho web client. Nhưng làm sao biết nó đúng mà không phải mở trình duyệt bấm thử mỗi lần? Phần sau nói về kiểm thử JS bằng QUnit/Hoot — bộ khung test frontend của Odoo, cách viết một test cho component và chạy nó.