Error Boundaries — работа с ошибками в React
Содержание
- Введение: проблема ошибок в React
- Что такое Error Boundary
- Создание Error Boundary (классовый компонент)
- Полноценный Error Boundary с fallback
- Обработка ошибок в событиях (try/catch)
- Обработка асинхронных ошибок
- Обработка ошибок в хуках
- Стратегии использования Error Boundaries
- Альтернативы Error Boundaries в функциональных компонентах
- Частые ошибки новичков
- Итог (что мы узнали)
- Задание для закрепления (на 1–2 часа)
- Что дальше?
Цель урока: научиться использовать Error Boundaries для отлова ошибок в React-компонентах. Понять, как предотвратить полный краш приложения, показывать пользователю красивые fallback-интерфейсы и логировать ошибки для отладки.
Введение: проблема ошибок в React
В обычном JavaScript ошибка в одном месте не всегда убивает всё приложение. В React всё иначе.
Проблема:
Если во время рендера компонента возникает ошибка, React размонтирует всё дерево компонентов. Пользователь видит пустой экран (White Screen of Death).
Почему так происходит:
- React не знает, как восстановиться после ошибки
- Безопаснее остановить всё, чем показать сломанный интерфейс
- Это защищает от каскадных ошибок
Главная мысль: Error Boundaries — это «ловушки» для ошибок. Они перехватывают ошибки в дочерних компонентах, показывают fallback-интерфейс и не дают приложению упасть целиком.
Что такое Error Boundary
Error Boundary — это компонент-обёртка, который отлавливает JavaScript-ошибки в своих дочерних компонентах во время:
- Рендера
- Жизненного цикла
- Конструкторов
Что НЕ ловит Error Boundary:
- Ошибки в обработчиках событий (onClick, onChange и т.д.)
- Ошибки в асинхронном коде (setTimeout, fetch)
- Ошибки на сервере (SSR)
- Ошибки в самом 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:
- ErrorBoundary — классовый компонент: принимает
fallbackкомпонент через пропс - Логирует ошибку в консоль
- В dev-режиме показывает детали ошибки (
stack trace) - Предоставляет метод сброса ошибки
SafeComponent — обёртка для обработки ошибок в событиях:
- Принимает
onClickи оборачивает его вtry/catch - Если ошибка — показывает
fallbackи кнопку «Повторить»
AsyncSafe — хук для асинхронных операций:
- Принимает async-функцию
- Возвращает
{ execute, loading, error, reset }
Дополнительно: создай компонент, который намеренно вызывает ошибку (в рендере, в обработчике события и асинхронно) и оберни его в разные виды Error Boundaries.
Проверка: убедись, что ошибка в одном компоненте не ломает всё приложение и показывает понятный fallback.
Пример выполненного задания
Скачать код выполненного задания с GitHub: (откроется в новой вкладке)Error Boundary в React.
Что дальше?
В следующем уроке разберём Fiber Architecture — внутреннее устройство React. Ты узнаешь, как React работает под капотом, что такое Fiber, как работает согласование (reconciliation) и почему React такой быстрый. Это не пригодится в повседневной разработке, но даст глубокое понимание технологии.