Hack Frontend Community

Error Boundaries в React

Что такое Error Boundaries?

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


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

До Error Boundaries ошибка в одном компоненте ломала всё приложение. Error Boundaries позволяют:

  • Предотвратить полную поломку приложения
  • Показать пользователю понятное сообщение об ошибке
  • Залогировать ошибку для анализа
  • Изолировать проблемные части приложения

Создание Error Boundary

Error Boundary — это классовый компонент с методами getDerivedStateFromError и/или componentDidCatch.

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Обновляем state, чтобы показать запасной UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Логируем ошибку в сервис отчётов
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Что-то пошло не так.</h1>;
    }

    return this.props.children;
  }
}

Использование

Оберните компоненты, в которых могут возникнуть ошибки:

function App() {
  return (
    <div>
      <ErrorBoundary>
        <MyWidget />
      </ErrorBoundary>
      
      <ErrorBoundary>
        <AnotherWidget />
      </ErrorBoundary>
    </div>
  );
}

Если MyWidget выбросит ошибку, упадёт только он, а AnotherWidget продолжит работать.


Разница между методами

getDerivedStateFromError

Вызывается во время рендеринга. Используется для обновления state и показа запасного UI.

static getDerivedStateFromError(error) {
  // Должен вернуть объект для обновления state
  return { hasError: true, error };
}

componentDidCatch

Вызывается после рендеринга. Используется для логирования ошибок.

componentDidCatch(error, errorInfo) {
  // errorInfo.componentStack содержит стек вызовов компонентов
  logErrorToService(error, errorInfo);
}

Продвинутый пример

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

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

  componentDidCatch(error, errorInfo) {
    this.setState({
      error,
      errorInfo
    });
    
    // Отправка в сервис мониторинга
    logErrorToService(error, errorInfo);
  }

  handleReset = () => {
    this.setState({
      hasError: false,
      error: null,
      errorInfo: null
    });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>Произошла ошибка</h2>
          <details>
            <summary>Подробности</summary>
            <p>{this.state.error && this.state.error.toString()}</p>
            <pre>{this.state.errorInfo && this.state.errorInfo.componentStack}</pre>
          </details>
          <button onClick={this.handleReset}>
            Попробовать снова
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

Что Error Boundaries НЕ ловят

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

  • В методах render
  • В хуках жизненного цикла
  • В конструкторах дочерних компонентов

НЕ перехватывают:

  • Ошибки в обработчиках событий
  • Асинхронный код (setTimeout, Promise)
  • Ошибки на стороне сервера (SSR)
  • Ошибки в самом Error Boundary

Обработчики событий

Для обработчиков событий используйте обычный try-catch:

function MyComponent() {
  function handleClick() {
    try {
      // Код, который может выбросить ошибку
      somethingDangerous();
    } catch (error) {
      console.error('Error in click handler:', error);
    }
  }

  return <button onClick={handleClick}>Click me</button>;
}

Асинхронные ошибки

Для асинхронных ошибок также нужен try-catch:

function MyComponent() {
  async function fetchData() {
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      // ...
    } catch (error) {
      console.error('Fetch error:', error);
    }
  }

  useEffect(() => {
    fetchData();
  }, []);

  return <div>...</div>;
}

Где размещать Error Boundaries

Глобальный уровень

function App() {
  return (
    <ErrorBoundary>
      <Router>
        <Layout>
          <Routes />
        </Layout>
      </Router>
    </ErrorBoundary>
  );
}

На уровне маршрутов

function App() {
  return (
    <Router>
      <ErrorBoundary>
        <Route path="/profile" element={<Profile />} />
      </ErrorBoundary>
      
      <ErrorBoundary>
        <Route path="/settings" element={<Settings />} />
      </ErrorBoundary>
    </Router>
  );
}

На уровне компонентов

function Dashboard() {
  return (
    <div>
      <ErrorBoundary>
        <ChartWidget />
      </ErrorBoundary>
      
      <ErrorBoundary>
        <StatsWidget />
      </ErrorBoundary>
    </div>
  );
}

Интеграция с сервисами мониторинга

Sentry

import * as Sentry from '@sentry/react';

class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    Sentry.captureException(error, { contexts: { react: errorInfo } });
  }

  // ...
}

// Или использовать готовый ErrorBoundary от Sentry
import { ErrorBoundary } from '@sentry/react';

function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <MyApp />
    </ErrorBoundary>
  );
}

React 18 и useErrorBoundary

В React пока нет хука для Error Boundaries, но библиотеки предлагают решения:

react-error-boundary

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

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

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // Сброс state приложения
      }}
      onError={(error, errorInfo) => {
        // Логирование
      }}
    >
      <MyApp />
    </ErrorBoundary>
  );
}

// Использование в функциональных компонентах
function MyComponent() {
  const handleError = useErrorHandler();

  async function fetchData() {
    try {
      const response = await fetch('/api/data');
      if (!response.ok) throw new Error('Failed to fetch');
      return response.json();
    } catch (error) {
      handleError(error);
    }
  }

  // ...
}

Best Practices

Несколько Error Boundaries

Не используйте один глобальный Error Boundary. Лучше несколько на разных уровнях:

function App() {
  return (
    <ErrorBoundary fallback={<GlobalError />}>
      <Header />
      
      <ErrorBoundary fallback={<SidebarError />}>
        <Sidebar />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<ContentError />}>
        <Content />
      </ErrorBoundary>
      
      <Footer />
    </ErrorBoundary>
  );
}

Информативные сообщения

Показывайте пользователю полезную информацию:

function ErrorFallback({ error }) {
  return (
    <div className="error-page">
      <h1>Упс! Произошла ошибка</h1>
      <p>Мы уже работаем над исправлением.</p>
      <p>Попробуйте:</p>
      <ul>
        <li>Обновить страницу</li>
        <li>Вернуться на главную</li>
        <li>Сообщить нам о проблеме</li>
      </ul>
      {process.env.NODE_ENV === 'development' && (
        <details>
          <summary>Техническая информация</summary>
          <pre>{error.message}</pre>
        </details>
      )}
    </div>
  );
}

Возможность восстановления

Дайте пользователю возможность попробовать снова:

class ErrorBoundary extends React.Component {
  state = { hasError: false, errorCount: 0 };

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

  componentDidUpdate(prevProps) {
    // Сбросить ошибку при изменении children
    if (this.state.hasError && prevProps.children !== this.props.children) {
      this.setState({ hasError: false, errorCount: 0 });
    }
  }

  handleReset = () => {
    this.setState(state => ({
      hasError: false,
      errorCount: state.errorCount + 1
    }));
  };

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h1>Ошибка</h1>
          <button onClick={this.handleReset}>Попробовать снова</button>
          {this.state.errorCount > 2 && (
            <p>Если проблема повторяется, обратитесь в поддержку.</p>
          )}
        </div>
      );
    }

    return this.props.children;
  }
}

Вывод

Error Boundaries:

  • Перехватывают ошибки в React-компонентах
  • Должны быть классовыми компонентами
  • Используют getDerivedStateFromError и componentDidCatch
  • Не ловят ошибки в обработчиках событий и асинхронном коде
  • Лучше использовать несколько на разных уровнях
  • Позволяют показать запасной UI вместо белого экрана
  • Важны для production приложений

На собеседовании:

Важно уметь:

  • Объяснить, что такое Error Boundaries
  • Показать, как создать Error Boundary
  • Рассказать, какие ошибки они не ловят
  • Объяснить разницу между getDerivedStateFromError и componentDidCatch
  • Привести примеры правильного размещения Error Boundaries