Menu

Error Boundary в React: перехват ошибок рендера

Граница ошибок это компонент, который перехватывает ошибки, выброшенные при рендере компонентов внутри него, и показывает запасной UI вместо пустого экрана. Что она перехватывает, как написать её классом, как её сбросить и библиотека react-error-boundary.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Граница ошибок (error boundary) это компонент React, который перехватывает ошибки, выброшенные при рендере компонентов внутри него, и показывает запасной UI вместо падения всего приложения. Это должен быть классовый компонент, который определяет static getDerivedStateFromError, потому что хука для этого нет. Вы пишете её один раз и оборачиваете ею любую часть дерева.

Нажмите «Break the profile», чтобы дочерний компонент выбросил ошибку во время рендера.

Профиль заменяется запасным UI, а остальная страница продолжает работать. Консоль показывает ошибку дважды: React логирует каждую перехваченную ошибку через console.error, а componentDidCatch логирует её ещё раз. «Try again» убирает причину (broken) и ошибку границы, поэтому профиль снова рендерится. Удалите this.props.onReset(); из reset, и Try again сразу вернёт вас в запасной UI, потому что причину ничто не исправило.

Как работает класс

Границей ошибок класс делают два метода:

  • static getDerivedStateFromError(error) выполняется во время рендеринга сразу после того, как дочерний компонент выбросил ошибку. Он возвращает обновление состояния, которое переключает границу на запасной UI. Он должен быть чистым: без логирования, без запросов.
  • componentDidCatch(error, info) выполняется после того, как запасной UI появился на экране. Используйте его для побочных эффектов, например отправки ошибки в сервис отчётов. info.componentStack перечисляет компонент, который выбросил ошибку, и компоненты над ним.
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;
    }
}

Эквивалента в виде хука нет, поэтому даже приложению, написанному только на функциональных компонентах, нужен этот один класс. Страница о классовых компонентах объясняет синтаксис классов, если он вам незнаком.

Что граница перехватывает, а что нет

Граница перехватывает ошибки, которые происходят, пока React работает с компонентами внутри неё:

  • рендеринг (тело функционального компонента, render класса),
  • методы жизненного цикла и конструкторы дочерних классовых компонентов,
  • эффекты (useEffect, useLayoutEffect) и ref-колбэки,
  • отклонённый промис, прочитанный через use, а в React 19 ошибку, выброшенную действием формы или функцией, переданной в startTransition из useTransition.

Она не перехватывает:

  • ошибки в обработчиках событий,
  • асинхронный код, который вы запускаете сами: setTimeout, колбэки .then(), await в обработчике события,
  • ошибки в собственном render границы (их перехватывает следующая граница выше),
  • ошибки во время серверного рендеринга.

Обработчики событий исключены, потому что не ломают рендеринг. Экран остаётся в корректном состоянии, поэтому try/catch в обработчике достаточно. Если вы всё же хотите показать границу, сохраните ошибку в состоянии и выбросьте её при следующем рендере:

Первая кнопка оставляет кнопки на экране и только выводит в консоль непойманную ошибку. Вторая перехватывает отклонение, сохраняет его, и управление берёт граница.

Где ставить границы

Одна граница вокруг всего приложения избавляет от белого экрана, но тогда любая мелкая ошибка заменяет всё. Ставьте границы вокруг частей, которые могут сломаться независимо: виджета на боковой панели, графика, каждого маршрута. Тогда сбой стоит пользователю одной панели, а не страницы.

Запасной UI показывает только панель Stocks. Поместите оба виджета в одну <ErrorBoundary>, и рабочая панель Weather исчезнет вместе с ней.

Без какой-либо границы React убирает с экрана всё дерево, когда ошибка рендера не перехвачена. Пустая страница безопаснее сломанной, которая показывает неверные данные, но пользователи воспринимают её как падение.

Библиотека react-error-boundary

Пакет react-error-boundary оборачивает класс за вас и добавляет помощники для сброса. В этом редакторе он недоступен, поэтому показан в статическом блоке:

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 сбрасывает границу, когда меняется любое из перечисленных значений, а showBoundary выполняет приём «сохранить и выбросить» из предыдущего примера.

Отчёты об ошибках в React 19

React 19 сообщает об ошибках по-новому. Ошибки, перехваченные границей, один раз логируются через console.error, а непойманные ошибки уходят в window.reportError. Чтобы отправлять их в собственный сервис, передайте опции в createRoot. Каждая опция заменяет соответствующее логирование по умолчанию:

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 нужен для ошибок, которые React исправил без вашей помощи, например несоответствия при гидратации или ошибки рендера, которая исчезла, когда React повторил попытку. hydrateRoot принимает те же три опции.

Частые ошибки

Граница внутри компонента, который выбрасывает ошибку. Граница перехватывает ошибки только от своих детей. Если Profile рендерит <ErrorBoundary> вокруг собственного JSX, а потом выбрасывает ошибку в своём теле, ошибка проходит мимо к следующей границе выше. Оборачивайте компонент из его родителя.

Побочные эффекты в getDerivedStateFromError. Он выполняется во время рендеринга и может выполниться больше одного раза, поэтому пусть он только возвращает состояние. Логированию место в componentDidCatch или в onCaughtError корня.

Сброс, который ничего не исправляет. Очистка ошибки границы рендерит тех же детей с теми же пропсами. Если они снова выбросят ошибку, пользователь вернётся к запасному UI. Сначала измените то, что вызвало ошибку (загрузите данные заново, очистите неверный ввод, смените key), затем очистите ошибку, как первый пример делает через onReset.

Одна граница на всё приложение и больше ничего. Она предотвращает пустую страницу, но превращает каждый мелкий сбой в ошибку на всю страницу. Добавьте границы вокруг функций, а верхнюю оставьте как последний рубеж.

Запасной UI, который прячет ошибку от вас. Дружелюбное сообщение подходит для пользователей, но убедитесь, что ошибка всё равно попадает в ваши логи. React 19 по умолчанию логирует перехваченные ошибки в консоль; в продакшене это консоль вашего пользователя, поэтому отправляйте ошибки в сервис из componentDidCatch или onCaughtError.

Границы ошибок естественно сочетаются с <Suspense>: Suspense отвечает за «ещё не готово», граница за «не удалось». Ставьте границу снаружи Suspense, как в примерах с use, чтобы один компонент покрывал загрузку, успех и сбой.

Часто задаваемые вопросы

Что такое граница ошибок в React?

Компонент, который перехватывает ошибки JavaScript, выброшенные при рендере любого компонента внутри него, и вместо этого рендерит запасной UI. Без неё ошибка во время рендера убирает с экрана всё приложение.

Есть ли хук для границ ошибок?

Нет. Граница ошибок должна быть классовым компонентом со static getDerivedStateFromError (и, при желании, componentDidCatch). Вы пишете её один раз и используете из функциональных компонентов как любой другой компонент или устанавливаете react-error-boundary, который оборачивает класс за вас.

Перехватывают ли границы ошибок ошибки в обработчиках событий?

Нет. Обработчики событий выполняются вне рендеринга, поэтому React не нужна граница для восстановления. Используйте try/catch в обработчике, а если хотите показать границу, сохраните ошибку в состоянии и выбросьте её во время рендера.

Перехватывают ли границы ошибок асинхронные ошибки?

Не из setTimeout, колбэков промисов или fetch().then(), которые вы запускаете сами. Они перехватывают отклонённый промис, прочитанный через use, а в React 19 и ошибку, выброшенную внутри действия формы или функции, переданной в startTransition из useTransition.

Как сбросить границу ошибок?

Очистите её состояние ошибки, обычно кнопкой Try again в запасном UI, после того как исправлено то, что вызвало ошибку. Смена key границы тоже её сбрасывает, потому что React монтирует новый экземпляр.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ