Menu

Error boundary trong React: bắt lỗi render bằng fallback

Error boundary là component bắt các lỗi bị ném ra khi render những component bên trong nó và hiển thị fallback thay vì một màn hình trắng. Tìm hiểu nó bắt được gì, cách viết nó bằng class, cách đặt lại nó và thư viện react-error-boundary.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Error boundary là một component React bắt các lỗi bị ném ra khi render những component bên trong nó và hiển thị giao diện dự phòng (fallback) thay vì làm sập cả ứng dụng. Nó phải là class component có định nghĩa static getDerivedStateFromError, vì không có hook nào làm được việc này. Bạn viết nó một lần rồi bọc quanh bất kỳ phần nào của cây component.

Nhấn "Break the profile" để component con ném lỗi trong lúc render.

Phần profile được thay bằng fallback, còn phần còn lại của trang vẫn hoạt động. Console hiện lỗi hai lần: React ghi mọi lỗi đã bắt bằng console.error, và componentDidCatch ghi lại một lần nữa. "Try again" xóa nguyên nhân (broken) và lỗi của boundary, nên profile render lại bình thường. Bỏ dòng this.props.onReset(); khỏi reset thì Try again sẽ ném ngay trở lại fallback, vì chưa có gì sửa nguyên nhân.

Class hoạt động thế nào

Hai phương thức biến một class thành error boundary:

  • static getDerivedStateFromError(error) chạy trong lúc render, ngay sau khi một component con ném lỗi. Nó trả về bản cập nhật state để chuyển boundary sang fallback. Nó phải thuần (pure): không ghi log, không gửi request.
  • componentDidCatch(error, info) chạy sau khi fallback đã hiện trên màn hình. Dùng nó cho các tác dụng phụ (side effect) như gửi lỗi đến một dịch vụ báo lỗi. info.componentStack liệt kê component đã ném lỗi và các component phía trên nó.
class ErrorBoundary extends Component {
    state = { error: null };

    static getDerivedStateFromError(error) {
        return { error }; // switch to the fallback
    }

    componentDidCatch(error, info) {
        reportError(error, info.componentStack); // your logging service
    }

    render() {
        if (this.state.error) return this.props.fallback;
        return this.props.children;
    }
}

Không có hook tương đương, nên ngay cả một ứng dụng chỉ viết bằng function component cũng cần đúng một class này. Trang class component giải thích cú pháp class nếu bạn chưa quen.

Boundary bắt được gì, và không bắt được gì

Boundary bắt các lỗi xảy ra khi React đang xử lý những component bên trong nó:

  • khi render (thân của function component, render của class),
  • các phương thức vòng đời và hàm khởi tạo của class component con,
  • effect (useEffect, useLayoutEffect) và ref callback,
  • một promise bị reject được đọc bằng use, và trong React 19 là lỗi ném ra từ form action hoặc từ hàm truyền vào startTransition lấy từ useTransition.

Nó không bắt:

  • lỗi trong trình xử lý sự kiện,
  • code bất đồng bộ do bạn tự khởi chạy: setTimeout, callback .then(), một await trong trình xử lý sự kiện,
  • lỗi trong chính render của boundary (boundary kế tiếp ở phía trên sẽ bắt),
  • lỗi khi render phía máy chủ.

Trình xử lý sự kiện bị loại ra vì chúng không làm hỏng quá trình render. Màn hình vẫn ở trạng thái hợp lệ, nên một try/catch trong handler là đủ. Nếu bạn vẫn muốn boundary hiện ra, hãy lưu lỗi vào state rồi ném nó ở lần render tiếp theo:

Nút đầu tiên giữ nguyên các nút trên màn hình và chỉ in một lỗi chưa được bắt trong console. Nút thứ hai bắt lấy lần reject, lưu nó lại, và boundary tiếp quản.

Đặt boundary ở đâu

Một boundary bọc cả ứng dụng giúp tránh màn hình trắng, nhưng khi đó bất kỳ lỗi nhỏ nào cũng thay thế toàn bộ nội dung. Hãy đặt boundary quanh những phần có thể hỏng độc lập: một widget ở thanh bên, một biểu đồ, từng route. Khi đó một lỗi chỉ khiến người dùng mất một khung, không phải cả trang.

Chỉ khung Stocks hiện fallback. Gộp cả hai widget vào một <ErrorBoundary> thì khung Weather đang chạy tốt cũng biến mất theo.

Không có boundary nào, React sẽ xóa toàn bộ cây khỏi màn hình khi một lỗi render không được bắt. Một trang trống an toàn hơn một trang hỏng hiển thị dữ liệu sai, nhưng người dùng sẽ coi đó là ứng dụng bị sập.

Thư viện react-error-boundary

Package react-error-boundary bọc sẵn class cho bạn và thêm các công cụ hỗ trợ đặt lại. Package này không có trong trình soạn thảo ở đây, nên ví dụ dưới đây là một khối code tĩnh:

import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';

function Fallback({ error, resetErrorBoundary }) {
    return (
        <div role="alert">
            <p>Something went wrong: {error.message}</p>
            <button onClick={resetErrorBoundary}>Try again</button>
        </div>
    );
}

<ErrorBoundary FallbackComponent={Fallback} onReset={() => refetch()} resetKeys={[userId]}>
    <Profile userId={userId} />
</ErrorBoundary>;

// Inside a child: hand an async error to the boundary
const { showBoundary } = useErrorBoundary();
fetchData().catch(showBoundary);

resetKeys đặt lại boundary khi bất kỳ giá trị nào trong danh sách thay đổi, còn showBoundary làm đúng mẹo "lưu rồi ném" trong ví dụ trước.

Báo lỗi trong React 19

React 19 báo lỗi theo cách mới. Lỗi được boundary bắt sẽ được ghi một lần bằng console.error, còn lỗi không được bắt sẽ đi đến window.reportError. Để gửi chúng đến dịch vụ của riêng bạn, hãy truyền tùy chọn vào createRoot. Mỗi tùy chọn thay thế cách ghi log mặc định tương ứng:

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'), {
    onCaughtError(error, errorInfo) {
        // an error boundary caught it
        logToService('caught', error, errorInfo.componentStack);
    },
    onUncaughtError(error, errorInfo) {
        // no boundary caught it, and React removed the tree
        logToService('uncaught', error, errorInfo.componentStack);
    },
    onRecoverableError(error, errorInfo) {
        // React recovered by itself, for example after a hydration mismatch
        logToService('recovered', error, errorInfo.componentStack);
    },
});
root.render(<App />);

onRecoverableError dành cho các lỗi React tự sửa được mà không cần bạn, như hydration mismatch hoặc một lỗi render biến mất khi React thử lại. hydrateRoot nhận đúng ba tùy chọn này.

Lỗi thường gặp

Đặt boundary bên trong chính component ném lỗi. Boundary chỉ bắt lỗi từ các component con của nó. Nếu Profile render <ErrorBoundary> quanh JSX của chính nó rồi ném lỗi ngay trong thân hàm, lỗi sẽ vượt qua boundary đó và đến boundary kế tiếp ở phía trên. Hãy bọc component từ component cha của nó.

Tác dụng phụ trong getDerivedStateFromError. Nó chạy trong lúc render và có thể chạy nhiều lần, nên chỉ dùng nó để trả về state. Việc ghi log thuộc về componentDidCatch hoặc onCaughtError của root.

Đặt lại mà không sửa gì. Xóa lỗi của boundary sẽ render lại đúng các component con với đúng props cũ. Nếu chúng ném lỗi lần nữa, người dùng lại quay về fallback. Hãy thay đổi nguyên nhân gây lỗi trước (fetch lại, xóa đầu vào sai, đổi key), rồi mới xóa lỗi, như ví dụ đầu tiên làm với onReset.

Chỉ một boundary cho cả ứng dụng, ngoài ra không có gì. Nó tránh được trang trống nhưng biến mọi lỗi nhỏ thành lỗi toàn trang. Hãy thêm boundary quanh từng tính năng, và giữ boundary cấp cao nhất làm phương án cuối cùng.

Fallback che mất lỗi khỏi bạn. Một thông báo thân thiện là đúng với người dùng, nhưng hãy chắc rằng lỗi vẫn đến được log của bạn. React 19 mặc định ghi lỗi đã bắt ra console; trên môi trường production, console đó là của người dùng, nên hãy gửi lỗi đến một dịch vụ từ componentDidCatch hoặc onCaughtError.

Error boundary đi rất ăn ý với <Suspense>: Suspense xử lý trạng thái "chưa sẵn sàng", boundary xử lý trạng thái "thất bại". Đặt boundary bên ngoài Suspense, như trong các ví dụ về use, để một component bao quát cả lúc đang tải, lúc thành công và lúc thất bại.

Câu hỏi thường gặp

Error boundary trong React là gì?

Là một component bắt các lỗi JavaScript bị ném ra khi render bất kỳ component nào bên trong nó, rồi render một giao diện dự phòng (fallback) thay thế. Không có error boundary, một lỗi khi render sẽ xóa toàn bộ ứng dụng khỏi màn hình.

Có hook nào cho error boundary không?

Không. Error boundary phải là class component có static getDerivedStateFromError (và componentDidCatch nếu cần). Bạn viết nó một lần rồi dùng từ function component như mọi component khác, hoặc cài react-error-boundary, thư viện bọc sẵn class đó cho bạn.

Error boundary có bắt lỗi trong trình xử lý sự kiện không?

Không. Trình xử lý sự kiện chạy ngoài quá trình render, nên React không cần boundary để khôi phục. Dùng try/catch trong handler, và nếu muốn boundary hiện ra, hãy lưu lỗi vào state rồi ném nó ra khi render.

Error boundary có bắt lỗi bất đồng bộ không?

Không bắt lỗi từ setTimeout, callback của promise hay fetch().then() do bạn tự khởi chạy. Nó có bắt một promise bị reject được đọc bằng use, và trong React 19 là lỗi ném ra bên trong form action hoặc hàm truyền vào startTransition lấy từ useTransition.

Làm sao để đặt lại một error boundary?

Xóa state lỗi của nó, thường bằng nút Try again trong fallback, sau khi đã sửa nguyên nhân gây lỗi. Đổi key của boundary cũng đặt lại nó, vì React sẽ mount một instance mới.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu