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