Хотите заказать веб-сайт? Связаться с нами

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

Цель урока: научиться использовать Error Boundaries для отлова ошибок в React-компонентах. Понять, как предотвратить полный краш приложения, показывать пользователю красивые fallback-интерфейсы и логировать ошибки для отладки.

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

Введение: проблема ошибок в React

В обычном JavaScript ошибка в одном месте не всегда убивает всё приложение. В React всё иначе.

Проблема:

Если во время рендера компонента возникает ошибка, React размонтирует всё дерево компонентов. Пользователь видит пустой экран (White Screen of Death).

Почему так происходит:

  1. React не знает, как восстановиться после ошибки
  2. Безопаснее остановить всё, чем показать сломанный интерфейс
  3. Это защищает от каскадных ошибок

Главная мысль: Error Boundaries — это «ловушки» для ошибок. Они перехватывают ошибки в дочерних компонентах, показывают fallback-интерфейс и не дают приложению упасть целиком.

Что такое Error Boundary

Error Boundary — это компонент-обёртка, который отлавливает JavaScript-ошибки в своих дочерних компонентах во время:

  1. Рендера
  2. Жизненного цикла
  3. Конструкторов

Что НЕ ловит Error Boundary:

  1. Ошибки в обработчиках событий (onClick, onChange и т.д.)
  2. Ошибки в асинхронном коде (setTimeout, fetch)
  3. Ошибки на сервере (SSR)
  4. Ошибки в самом Error Boundary

Error Boundaries работают только в классовых компонентах! В функциональных компонентах нет аналога (но есть библиотеки-заменители).

Создание Error Boundary (классовый компонент)

Базовый синтаксис:

import React from 'react';

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

  // 1. Метод для получения ошибки
  static getDerivedStateFromError(error) {
    // Обновляем состояние, чтобы показать fallback
    return { hasError: true, error };
  }

  // 2. Метод для логирования ошибки
  componentDidCatch(error, errorInfo) {
    // Логируем ошибку в сервис аналитики
    console.error('Ошибка в компоненте:', error);
    console.error('Информация об ошибке:', errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Показываем fallback UI
      return <h1>Что-то пошло не так</h1>;
    }

    return this.props.children;
  }
}

// Использование
function App() {
  return (
    <ErrorBoundary>
      <BuggyComponent />
    </ErrorBoundary>
  );
}

Полноценный Error Boundary с fallback

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

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

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

  render() {
    if (this.state.hasError) {
      // Можно использовать кастомный fallback
      if (this.props.fallback) {
        return this.props.fallback({
          error: this.state.error,
          reset: this.resetError
        });
      }

      // Или дефолтный
      return (
        <div style={styles.container}>
          <h2 style={styles.title}>😕 Что-то пошло не так</h2>
          <p style={styles.message}>
            Произошла ошибка. Попробуйте обновить страницу.
          </p>
          {process.env.NODE_ENV === 'development' && (
            <details style={styles.details}>
              <summary>Детали ошибки</summary>
              <pre>{this.state.error?.toString()}</pre>
              <pre>{this.state.errorInfo?.componentStack}</pre>
            </details>
          )}
          <button onClick={this.resetError} style={styles.button}>
            Попробовать снова
          </button>
        </div>
      );
    }

    return this.props.children;
  }

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

const styles = {
  container: {
    padding: '20px',
    background: '#fff3f3',
    border: '1px solid #ff6b6b',
    borderRadius: '8px',
    margin: '20px'
  },
  title: { color: '#d32f2f', marginTop: 0 },
  message: { color: '#666' },
  details: { 
    background: '#f5f5f5', 
    padding: '10px', 
    borderRadius: '4px',
    marginTop: '10px'
  },
  button: {
    padding: '8px 16px',
    background: '#d32f2f',
    color: 'white',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer',
    fontSize: '14px'
  }
};

// Использование
function App() {
  return (
    <ErrorBoundary
      fallback={({ error, reset }) => (
        <div>
          <h3>Ошибка: {error.message}</h3>
          <button onClick={reset}>Перезагрузить</button>
        </div>
      )}
    >
      <BuggyComponent />
    </ErrorBoundary>
  );
}

Обработка ошибок в событиях (try/catch)

Error Boundary не ловит ошибки в обработчиках событий. Их нужно обрабатывать вручную:

function ButtonWithError() {
  const handleClick = () => {
    try {
      // Код, который может упасть
      throw new Error('Ошибка в обработчике клика');
    } catch (error) {
      // Обрабатываем ошибку здесь
      console.error('Поймали ошибку:', error);
      
      // Можно показать уведомление пользователю
      notifyUser('Что-то пошло не так');
    }
  };

  return <button onClick={handleClick}>Кликни меня</button>;
}

Или через хук-обёртку:

function useErrorHandler() {
  const [error, setError] = useState(null);

  const handleError = (error) => {
    setError(error);
    // Логируем ошибку
    console.error(error);
  };

  const clearError = () => setError(null);

  return { error, handleError, clearError };
}

function SafeButton({ onClick, children }) {
  const { error, handleError, clearError } = useErrorHandler();

  const handleClick = () => {
    try {
      onClick();
      clearError();
    } catch (err) {
      handleError(err);
    }
  };

  if (error) {
    return <p style={{ color: 'red' }}>Ошибка: {error.message}</p>;
  }

  return <button onClick={handleClick}>{children}</button>;
}

Обработка асинхронных ошибок

Асинхронные ошибки тоже не ловятся Error Boundary:

function AsyncComponent() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        if (!response.ok) throw new Error('Ошибка загрузки');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
        console.error('Ошибка:', err);
      }
    };

    fetchData();
  }, []);

  if (error) {
    return <p>Ошибка: {error.message}</p>;
  }

  if (!data) {
    return <p>Загрузка...</p>;
  }

  return <div>{data}</div>;
}

Обработка ошибок в хуках

Можно создать кастомный хук для обработки ошибок:

function useAsyncError() {
  const [error, setError] = useState(null);

  const throwError = (error) => {
    setError(error);
    throw error;
  };

  return { error, throwError };
}

function ComponentWithHook() {
  const { error, throwError } = useAsyncError();

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(res => {
        if (!res.ok) throw new Error('Ошибка загрузки');
        return res.json();
      })
      .catch(err => throwError(err));
  }, [throwError]);

  if (error) {
    return <p>Ошибка: {error.message}</p>;
  }

  return <p>Данные загружены</p>;
}

Стратегии использования Error Boundaries

1. Глобальный Error Boundary (на уровне App):

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

2. Локальные Error Boundary (для отдельных компонентов):

function Dashboard() {
  return (
    <div>
      <h1>Панель управления</h1>
      
      <ErrorBoundary fallback={<WidgetError />}>
        <UserStatsWidget />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<WidgetError />}>
        <SalesWidget />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<WidgetError />}>
        <AnalyticsWidget />
      </ErrorBoundary>
    </div>
  );
}

3. Вложенные Error Boundary (разные уровни):

<ErrorBoundary fallback={<PageError />}>
  <Page>
    <ErrorBoundary fallback={<SectionError />}>
      <Section>
        <ErrorBoundary fallback={<ComponentError />}>
          <BuggyComponent />
        </ErrorBoundary>
      </Section>
    </ErrorBoundary>
  </Page>
</ErrorBoundary>

Альтернативы Error Boundaries в функциональных компонентах

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

npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';

function FallbackComponent({ error, resetErrorBoundary }) {
  return (
    <div>
      <h3>Ошибка: {error.message}</h3>
      <button onClick={resetErrorBoundary}>Попробовать снова</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={FallbackComponent}
      onError={(error, errorInfo) => {
        console.error('Ошибка:', error);
        // Логирование в сервис
      }}
      onReset={() => {
        // Сброс состояния приложения
        resetAppState();
      }}
    >
      <BuggyComponent />
    </ErrorBoundary>
  );
}

Кастомный хук для обработки ошибок:

function useErrorBoundary() {
  const [error, setError] = useState(null);

  const handleError = (error) => {
    setError(error);
  };

  const resetError = () => {
    setError(null);
  };

  return { error, handleError, resetError };
}

function SafeComponent({ children }) {
  const { error, handleError, resetError } = useErrorBoundary();

  if (error) {
    return (
      <div>
        <p>Ошибка: {error.message}</p>
        <button onClick={resetError}>Попробовать снова</button>
      </div>
    );
  }

  return (
    <ErrorBoundary onError={handleError}>
      {children}
    </ErrorBoundary>
  );
}

Частые ошибки новичков

Ошибка Почему Исправление
Пытаются использовать функциональный компонент Error Boundary только классовые Используй класс или библиотеку
Не ловят ошибки в событиях Error Boundary не работает с событиями Используй try/catch
Не ловят асинхронные ошибки Error Boundary не работает с async Используй try/catch в async
Используют один Boundary для всего Сложно локализовать ошибку Используй несколько Boundary
Не логируют ошибки Сложно отлаживать Логируй в componentDidCatch

Итог (что мы узнали)

Что узнали Как это работает
Error Boundary Перехват ошибок в дочерних компонентах
getDerivedStateFromError Обновление состояния при ошибке
componentDidCatch Логирование ошибки
Fallback UI Альтернативный интерфейс при ошибке
Ошибки в событиях Обрабатываются через try/catch
Асинхронные ошибки Обрабатываются через try/catch
Стратегии Глобальные и локальные Boundary

Задание для закрепления (на 1–2 часа)

Создай систему Error Boundaries:

  1. ErrorBoundary — классовый компонент: принимает fallback компонент через пропс
  2. Логирует ошибку в консоль
  3. В dev-режиме показывает детали ошибки (stack trace)
  4. Предоставляет метод сброса ошибки

SafeComponent — обёртка для обработки ошибок в событиях:

  1. Принимает onClick и оборачивает его в try/catch
  2. Если ошибка — показывает fallback и кнопку «Повторить»

AsyncSafe — хук для асинхронных операций:

  1. Принимает async-функцию
  2. Возвращает { execute, loading, error, reset }

Дополнительно: создай компонент, который намеренно вызывает ошибку (в рендере, в обработчике события и асинхронно) и оберни его в разные виды Error Boundaries.

Проверка: убедись, что ошибка в одном компоненте не ломает всё приложение и показывает понятный fallback.

Пример выполненного задания

Скачать код выполненного задания с GitHub: (откроется в новой вкладке)Error Boundary в React.

Что дальше?

В следующем уроке разберём Fiber Architecture — внутреннее устройство React. Ты узнаешь, как React работает под капотом, что такое Fiber, как работает согласование (reconciliation) и почему React такой быстрый. Это не пригодится в повседневной разработке, но даст глубокое понимание технологии.

Теги: