ReactMiddleTechnical

Как работает обработка ошибок в React? Что такое Error Boundaries?

Error Boundary — класс с static getDerivedStateFromError и componentDidCatch, который ловит ошибки рендера, lifecycle и конструкторов потомков и показывает fallback вместо падения всего дерева. Не ловит ошибки в обработчиках событий, async-коде вне рендера и SSR hydration mismatch.

Разбор

React реализует модель «защитной границы»: ошибки в render-фазе пробрасываются вверх по дереву, пока не натолкнутся на компонент, реализующий контракт Error Boundary. Без такой границы ошибка размонтирует всё дерево, и пользователь увидит пустой экран.

Что происходит внутри

React ищет ближайшего предка с static getDerivedStateFromError(error) или componentDidCatch(error, info). Первый метод возвращает новое state — компонент рендерит fallback. Второй принимает error и info.componentStack и удобен для логирования (Sentry, Datadog).

Граница ловит:

  • ошибки в render-фазе функциональных и классовых компонентов;
  • ошибки в constructor/lifecycle классов;
  • rejected-промисы через use(promise), если они доходят до commit-фазы и не перехвачены вложенной границей.

Граница не ловит:

  • ошибки в обработчиках событий (onClick, onChange) — их ловят локальным try/catch;
  • ошибки в setTimeout, fetch, async-функциях вне render-фазы;
  • SSR hydration mismatch — React пытается восстановиться через client-only rerender самостоятельно.

Хука для Error Boundary до сих пор нет. Класс — единственный официальный способ. Сообщество использует react-error-boundary, которая даёт функциональный API поверх класса.

Пример

import { Component, type ReactNode } from "react";

type Props = { fallback: ReactNode; children: ReactNode };
type State = { error?: Error };

export class ErrorBoundary extends Component<Props, State> {
  state: State = {};

  static getDerivedStateFromError(error: Error): State {
    return { error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    // Логирование в Sentry, Datadog и т.д.
    console.error("[ErrorBoundary]", error, info.componentStack);
  }

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

// Использование
function WidgetPage() {
  return (
    <>
      <ErrorBoundary fallback={<p>Не удалось загрузить виджет A</p>}>
        <WidgetA />
      </ErrorBoundary>
      <ErrorBoundary fallback={<p>Не удалось загрузить виджет B</p>}>
        <WidgetB />
      </ErrorBoundary>
    </>
  );
}

// С react-error-boundary
import { ErrorBoundary } from "react-error-boundary";
<ErrorBoundary
  fallbackRender={({ error, resetErrorBoundary }) => (
    <div>
      <p>Ошибка: {error.message}</p>
      <button onClick={resetErrorBoundary}>Попробовать снова</button>
    </div>
  )}
  onError={(error, info) => Sentry.captureException(error, { extra: info })}
>
  <HeavyComponent />
</ErrorBoundary>

Оборачивайте независимые поддеревья (виджет, страница), а не всё приложение — ошибка одного виджета не должна гасить остальные.

Подводные камни

  • Граница не ловит ошибки внутри себя. Если ошибка возникает в render самой границы или в её fallback, экран снова падает. Делайте fallback максимально простым, без асинхронных запросов и сложной логики.
  • Ошибки в обработчиках событий невидимы для границы. onClick, onChange — это не render-фаза. Ловите их локально try/catch и отражайте в state: const [error, setError] = useState(null).
  • Logginng может сам упасть. componentDidCatch в production должен быть устойчив к ошибкам сервиса логирования — оберните в try/catch или убедитесь, что Sentry/Datadog SDK не бросает.
  • Пары Suspense + ErrorBoundary обязательны. Rejected-промис от use() ловится Error Boundary. Без неё в паре с Suspense — пустой экран без объяснений. Всегда держите ErrorBoundary рядом с Suspense.
  • SSR-стриминг удваивает логирование. При ошибке ниже Suspense-границы в Next.js стриминге сервер и клиент могут оба зафиксировать одну ошибку. Дедублируйте в Sentry через fingerprint или фильтрацию по environment.
  • Кнопка «Попробовать снова» требует сброса state. Boundary перейдёт обратно в нормальный режим только если её state error сбросить. В react-error-boundary это делает resetErrorBoundary(); в самописной реализации нужно явно вызывать setState({ error: undefined }).
  • Не путайте с window.onerror и unhandledrejection. Error Boundary видит контекст React-дерева (component stack, props). Глобальные обработчики видят все ошибки JS, включая асинхронные. Они дополняют друг друга — используйте оба.
  • В dev-mode поведение отличается. React 18+ в development кратковременно показывает overlay со стек-трейсом перед тем, как передать ошибку в boundary. В production overlay нет. Не удивляйтесь, если в dev всё «мигает» даже при корректно работающей границе.

Common mistakes

  • Ожидать, что граница поймает ошибку в onClick.
  • Делать одну границу на всё приложение.
  • Класть в fallback компонент, который сам может упасть.

What the interviewer is testing

  • Знает ли, какие ошибки ловятся, а какие нет.
  • Понимает ли отсутствие хука и причины этого.
  • Применяет ли границы вместе с Suspense.

Sources

Related topics