Error Boundaries: создаем надежные React-приложения

16 марта 2026
Автор

Олег Марков

Error Boundaries — обработка ошибок в React

Error Boundaries — это React-компоненты, которые перехватывают JavaScript-ошибки в дереве дочерних компонентов, логируют их и показывают запасной UI вместо упавшего дерева компонентов.

// Базовый пример Error Boundary
class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('Поймана ошибка:', error, info);
  }

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

// Использование
<ErrorBoundary fallback={<p>Что-то пошло не так</p>}>
  <MyComponent />
</ErrorBoundary>

Зачем нужны Error Boundaries

В React 16+ необработанная ошибка в компоненте размонтирует всё дерево приложения. Это лучше, чем показывать сломанный UI — но полный краш неприемлем для продакшена.

Error Boundaries позволяют:

  • Изолировать ошибки — сбой в одной части не ронит всё приложение
  • Показывать fallback UI — понятное сообщение вместо белого экрана
  • Логировать ошибки — отправлять отчёты в Sentry, Datadog и т.д.
  • Восстанавливаться — предоставить кнопку "Попробовать снова"

Реализация на классовых компонентах

Error Boundaries можно создать только как классовые компоненты — функциональные компоненты пока не поддерживают getDerivedStateFromError и componentDidCatch.

// components/ErrorBoundary.tsx
import React from 'react';

interface Props {
  children: React.ReactNode;
  fallback?: React.ReactNode;
  onError?: (error: Error, info: React.ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  // Вызывается при рендере после ошибки
  // Обновляет state для показа fallback UI
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  // Вызывается после рендера с ошибкой
  // Используется для логирования
  componentDidCatch(error: Error, info: React.ErrorInfo) {
    // Логируем в внешний сервис
    this.props.onError?.(error, info);
    console.error('ErrorBoundary поймал:', error);
    console.error('Component stack:', info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div className="error-fallback">
          <h2>Что-то пошло не так</h2>
          <p>{this.state.error?.message}</p>
        </div>
      );
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

Два метода жизненного цикла

getDerivedStateFromError

Статический метод, вызываемый во время фазы рендера после ошибки. Должен возвращать новый state.

static getDerivedStateFromError(error: Error) {
  // ✅ Только возвращаем новый state
  return { hasError: true, error };
  // ❌ Нельзя вызывать side effects здесь
}

componentDidCatch

Вызывается после коммита (phase commit). Используется для side effects — логирования, отчётов об ошибках.

componentDidCatch(error: Error, info: React.ErrorInfo) {
  // ✅ Side effects — логирование, аналитика
  logErrorToService(error, info.componentStack);
}

Продвинутый Error Boundary с восстановлением

// components/ErrorBoundary.tsx
import React from 'react';
import { logError } from '@/lib/monitoring';

interface Props {
  children: React.ReactNode;
  fallback?: (props: FallbackProps) => React.ReactNode;
  onReset?: () => void;
}

interface FallbackProps {
  error: Error;
  resetError: () => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends React.Component<Props, State> {
  state: State = { hasError: false, error: null };

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

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    // Отправляем в мониторинг (Sentry, Datadog, etc.)
    logError(error, {
      componentStack: info.componentStack,
      timestamp: new Date().toISOString(),
    });
  }

  // Сброс состояния ошибки
  handleReset = () => {
    this.props.onReset?.();
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError && this.state.error) {
      // Кастомный fallback с возможностью восстановления
      if (this.props.fallback) {
        return this.props.fallback({
          error: this.state.error,
          resetError: this.handleReset,
        });
      }

      // Дефолтный fallback
      return (
        <div className="flex flex-col items-center gap-4 p-8">
          <h2 className="text-xl font-semibold text-red-600">
            Произошла ошибка
          </h2>
          <p className="text-gray-600">{this.state.error.message}</p>
          <button
            onClick={this.handleReset}
            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
          >
            Попробовать снова
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Оборачивание функциональным компонентом

Для удобства использования хуков и props можно создать обёртку:

// components/WithErrorBoundary.tsx
'use client';

import ErrorBoundary from './ErrorBoundary';
import { useRouter } from 'next/navigation';

interface Props {
  children: React.ReactNode;
  redirectOnError?: string;
}

export function WithErrorBoundary({ children, redirectOnError }: Props) {
  const router = useRouter();

  const handleReset = () => {
    if (redirectOnError) {
      router.push(redirectOnError);
    }
  };

  return (
    <ErrorBoundary
      onReset={handleReset}
      fallback={({ error, resetError }) => (
        <div className="error-container">
          <h2>Ошибка: {error.message}</h2>
          <button onClick={resetError}>Попробовать снова</button>
        </div>
      )}
    >
      {children}
    </ErrorBoundary>
  );
}

Использование библиотеки react-error-boundary

Популярная библиотека react-error-boundary предоставляет готовое решение с поддержкой функциональных компонентов:

npm install react-error-boundary
// Базовое использование
import { ErrorBoundary } from 'react-error-boundary';

function FallbackComponent({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert">
      <p>Что-то пошло не так:</p>
      <pre style={{ color: 'red' }}>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Попробовать снова</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={FallbackComponent}
      onError={(error, info) => logError(error, info)}
      onReset={() => {
        // Сброс состояния приложения при необходимости
      }}
    >
      <MyWidget />
    </ErrorBoundary>
  );
}
// Хук useErrorBoundary для программного выброса ошибок
import { useErrorBoundary } from 'react-error-boundary';

function MyComponent() {
  const { showBoundary } = useErrorBoundary();

  const handleAsyncError = async () => {
    try {
      await fetchSomeData();
    } catch (error) {
      // Передаём async-ошибку в ближайший Error Boundary
      showBoundary(error);
    }
  };

  return <button onClick={handleAsyncError}>Загрузить данные</button>;
}

Интеграция с Next.js

App Router — error.tsx

В Next.js App Router есть встроенная поддержка через файл error.tsx:

// app/dashboard/error.tsx
'use client'; // Обязательно — error.tsx должен быть Client Component

import { useEffect } from 'react';

interface Props {
  error: Error & { digest?: string };
  reset: () => void;
}

export default function DashboardError({ error, reset }: Props) {
  useEffect(() => {
    // Логируем ошибку в сервис мониторинга
    console.error('Dashboard error:', error);
  }, [error]);

  return (
    <div className="flex flex-col items-center gap-4 p-8">
      <h2 className="text-2xl font-bold">Ошибка в дашборде</h2>
      <p className="text-gray-500">{error.message}</p>
      <button
        onClick={reset}
        className="px-4 py-2 bg-primary text-white rounded"
      >
        Повторить попытку
      </button>
    </div>
  );
}
// app/dashboard/global-error.tsx — перехватывает ошибки в корневом layout
'use client';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <h2>Критическая ошибка приложения</h2>
        <button onClick={reset}>Перезагрузить</button>
      </body>
    </html>
  );
}

Стратегия изоляции ошибок

// app/layout.tsx — несколько уровней Error Boundary
import ErrorBoundary from '@/components/ErrorBoundary';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {/* Глобальный boundary для критических ошибок */}
        <ErrorBoundary fallback={<CriticalErrorPage />}>
          <Header />
          {/* Изолированный boundary для основного контента */}
          <ErrorBoundary fallback={<ContentErrorFallback />}>
            <main>{children}</main>
          </ErrorBoundary>
          <Footer />
        </ErrorBoundary>
      </body>
    </html>
  );
}

Ограничения Error Boundaries

Error Boundaries НЕ перехватывают ошибки:

Тип ошибки Перехватывается? Решение
Ошибки в обработчиках событий try/catch в обработчике
Асинхронный код (setTimeout, fetch) useErrorBoundary() из react-error-boundary
Server-side rendering (SSR) error.tsx в Next.js App Router
Ошибки в самом Error Boundary Вышестоящий Error Boundary
Ошибки при рендере дочерних компонентов getDerivedStateFromError
Ошибки в конструкторе дочерних компонентов getDerivedStateFromError
// ❌ Плохо — ошибка в обработчике события не поймается Error Boundary
function BadComponent() {
  const handleClick = () => {
    throw new Error('Ошибка в обработчике'); // Не поймается!
  };
  return <button onClick={handleClick}>Нажми</button>;
}

// ✅ Хорошо — используем try/catch для обработчиков событий
function GoodComponent() {
  const { showBoundary } = useErrorBoundary();

  const handleClick = () => {
    try {
      riskyOperation();
    } catch (error) {
      showBoundary(error); // Пробрасываем в Error Boundary
    }
  };

  return <button onClick={handleClick}>Нажми</button>;
}

Антипаттерны

// ❌ Один Error Boundary на всё приложение
// При любой ошибке весь UI заменяется fallback
function App() {
  return (
    <ErrorBoundary fallback={<Fallback />}>
      <Header />
      <Sidebar />
      <MainContent />  {/* Ошибка здесь обнуляет весь интерфейс */}
      <Footer />
    </ErrorBoundary>
  );
}

// ✅ Хорошо — гранулярная изоляция
function App() {
  return (
    <div>
      <Header /> {/* Критичный UI — без boundary или с отдельным */}
      <ErrorBoundary fallback={<SidebarError />}>
        <Sidebar />
      </ErrorBoundary>
      <ErrorBoundary fallback={<ContentError />}>
        <MainContent />
      </ErrorBoundary>
      <Footer />
    </div>
  );
}
// ❌ Плохо — игнорирование логирования
static getDerivedStateFromError(error: Error) {
  return { hasError: true };
  // Ошибка нигде не логируется!
}

// ✅ Хорошо — всегда логируем в componentDidCatch
componentDidCatch(error: Error, info: React.ErrorInfo) {
  // Отправляем в Sentry
  Sentry.captureException(error, {
    contexts: { react: { componentStack: info.componentStack } },
  });
}

Лучшие практики

  1. Гранулярность — оборачивайте отдельные независимые части UI, а не всё приложение
  2. Информативный fallback — показывайте понятное сообщение и возможность восстановления
  3. Всегда логируйте — отправляйте ошибки в систему мониторинга (Sentry, Datadog)
  4. Кнопка "Попробовать снова" — дайте пользователю возможность восстановить работу
  5. Используйте react-error-boundary — готовое решение с поддержкой хуков
  6. Отдельный boundary для критичных секций — Header, навигация — изолируйте от остального
  7. Тестируйте Error Boundaries — убедитесь что fallback отображается корректно
  8. Не злоупотребляйте — не нужен boundary для каждого компонента

Сравнение подходов

Подход Когда использовать Плюсы Минусы
Классовый ErrorBoundary Кастомная логика восстановления Полный контроль Verbose синтаксис
react-error-boundary Большинство случаев Готовое решение, хуки Зависимость
Next.js error.tsx Сегменты маршрутов Встроено в Next.js Только App Router
Next.js global-error.tsx Ошибки в root layout Перехват критических ошибок Заменяет весь layout

Краткое резюме

Концепция Суть
Error Boundary Классовый компонент, перехватывающий ошибки рендера
getDerivedStateFromError Обновляет state для показа fallback (фаза рендера)
componentDidCatch Логирует ошибку (фаза коммита)
Fallback UI Запасной интерфейс при ошибке
useErrorBoundary Пробрасывает async-ошибки в boundary
error.tsx Next.js App Router аналог Error Boundary

Дополнительные материалы

Стрелочка влевоuseMemo: как спасти производительность от тяжелых вычисленийКонтролируемые компоненты в React: полный контроль над формамиСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIКомпозиция компонентов в React: строим гибкие интерфейсыCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с данными
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
StrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOM
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий