Представьте: пользователь открывает ваш интернет-магазин. В одном из компонентов списка товаров приходит null вместо объекта — и React, не зная как отрисовать null.price, бросает ошибку. Без Error Boundary весь экран становится белым. Пользователь видит пустую страницу и уходит.
Error Boundary — это React-компонент, который перехватывает ошибки рендеринга в дочерних компонентах и вместо белого экрана показывает запасной UI (fallback).
Error Boundary — это классовый компонент с двумя специальными методами жизненного цикла:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
// Вызывается при ошибке рендеринга — обновляет state
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
// Вызывается после getDerivedStateFromError — для логирования
componentDidCatch(error, errorInfo) {
console.error('Ошибка компонента:', error)
console.error('Стек компонентов:', errorInfo.componentStack)
// Отправляем в Sentry, Datadog, etc.
logErrorToService(error, errorInfo)
}
render() {
if (this.state.hasError) {
return <h2>Что-то пошло не так. Попробуйте обновить страницу.</h2>
}
return this.props.children
}
}
// Использование:
<ErrorBoundary>
<UserProfile userId={id} /> {/* если здесь ошибка — показывается fallback */}
</ErrorBoundary>| Метод | Когда вызывается | Для чего |
|---|---|---|
| getDerivedStateFromError | Во время рендеринга (фаза render) | Обновить state → показать fallback UI |
| componentDidCatch | После рендеринга (фаза commit) | Логировать ошибку, side effects |
Важно: getDerivedStateFromError — статический метод (static), у него нет доступа к this. Он должен быть чистой функцией.
Error Boundary перехватывает только ошибки рендеринга (метод render и lifecycle методы). Следующие ошибки он не ловит:
setTimeout, fetch, промисы — используйте try/catchonClick — используйте try/catch внутри// ОШИБКА: это НЕ поймает Error Boundary
function BrokenButton() {
const handleClick = () => {
throw new Error('Ошибка в обработчике') // не поймается!
}
return <button onClick={handleClick}>Нажми</button>
}
// ПРАВИЛЬНО: try/catch в обработчике
function FixedButton() {
const handleClick = () => {
try {
riskyOperation()
} catch (error) {
setError(error.message)
}
}
return <button onClick={handleClick}>Нажми</button>
}Писать классовый компонент каждый раз — неудобно. Библиотека react-error-boundary предоставляет готовое решение:
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Произошла ошибка:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Попробовать снова</button>
</div>
)
}
// Использование с кнопкой сброса:
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => logError(error, info)}
onReset={() => {
// сбросить состояние приложения перед повтором
}}
>
<UserDashboard />
</ErrorBoundary>Одна из лучших возможностей react-error-boundary — кнопка "Попробовать снова". При нажатии компонент размонтируется и монтируется заново:
import { useErrorBoundary } from 'react-error-boundary'
// Программный сброс из дочернего компонента:
function UserProfile() {
const { showBoundary } = useErrorBoundary()
async function fetchUser() {
try {
const data = await api.getUser()
setUser(data)
} catch (error) {
showBoundary(error) // пробрасываем ошибку в boundary
}
}
}Не нужно оборачивать каждый компонент. Подумайте, какие части UI независимы:
// Хорошо: изолируем виджеты
<ErrorBoundary fallback={<p>Ошибка виджета</p>}>
<WeatherWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Ошибка чата</p>}>
<ChatWidget />
</ErrorBoundary>
// Если упадёт WeatherWidget — ChatWidget продолжит работатьОбщее правило: один Error Boundary на каждую независимую часть интерфейса.
Реализация паттерна Error Boundary через JavaScript: перехват ошибок, логирование, сброс состояния и изолированные зоны ошибок
// Демонстрируем концепцию Error Boundary на чистом JavaScript.
// В React это классовый компонент, здесь — функция-обёртка с тем же поведением.
// --- Симуляция компонентов ---
function simulateRender(componentName, props) {
// Некоторые компоненты "падают" при определённых данных
if (componentName === 'UserCard' && props.user === null) {
throw new Error('Cannot read property "name" of null')
}
if (componentName === 'PriceDisplay' && props.price < 0) {
throw new Error('Недопустимая цена: ' + props.price)
}
return '[rendered: ' + componentName + ']'
}
// --- Error Boundary: паттерн обёртки ---
function createErrorBoundary(options = {}) {
const { onError, fallback = 'Что-то пошло не так', resetOnRetry = true } = options
let state = {
hasError: false,
error: null,
retryCount: 0,
}
function getDerivedStateFromError(error) {
// Аналог статического метода в React
return { hasError: true, error }
}
function componentDidCatch(error, context) {
// Аналог метода жизненного цикла
console.error('[ErrorBoundary] Поймана ошибка:', error.message)
console.error('[ErrorBoundary] Контекст:', context)
if (onError) onError(error, context)
}
function render(renderFn, context) {
if (state.hasError) {
console.log('[ErrorBoundary] Показываем fallback: "' + fallback + '"')
console.log('[ErrorBoundary] Ошибка была:', state.error.message)
return { output: fallback, error: state.error }
}
try {
const output = renderFn()
return { output, error: null }
} catch (error) {
const newState = getDerivedStateFromError(error)
state = { ...state, ...newState }
componentDidCatch(error, context || 'неизвестный компонент')
// После обновления state — рендерим снова (теперь покажет fallback)
return render(renderFn, context)
}
}
function reset() {
state = { hasError: false, error: null, retryCount: state.retryCount + 1 }
console.log('[ErrorBoundary] Сброшен. Попытка #' + state.retryCount)
}
return { render, reset, getState: () => ({ ...state }) }
}
// --- Тест 1: Компонент падает ---
console.log('=== Тест 1: Компонент падает ===')
const boundary1 = createErrorBoundary({
fallback: 'Ошибка загрузки профиля',
onError: (err) => console.log('[Логирование] Отправка в Sentry:', err.message),
})
const result1 = boundary1.render(
() => simulateRender('UserCard', { user: null }),
'UserCard'
)
console.log('Вывод:', result1.output) // fallback
console.log('Состояние:', boundary1.getState())
// --- Тест 2: Компонент работает нормально ---
console.log('
=== Тест 2: Нормальная работа ===')
const boundary2 = createErrorBoundary({ fallback: 'Ошибка цены' })
const result2 = boundary2.render(
() => simulateRender('PriceDisplay', { price: 1999 }),
'PriceDisplay'
)
console.log('Вывод:', result2.output) // нормальный рендер
console.log('Есть ошибка:', boundary2.getState().hasError) // false
// --- Тест 3: Сброс и повтор ---
console.log('
=== Тест 3: Сброс после ошибки ===')
const boundary3 = createErrorBoundary({ fallback: 'Временная ошибка' })
let shouldFail = true
// Первый рендер — упадёт
boundary3.render(() => {
if (shouldFail) throw new Error('Временный сбой API')
return 'Данные загружены'
})
console.log('После ошибки — hasError:', boundary3.getState().hasError)
// "Исправляем" проблему и нажимаем Retry
shouldFail = false
boundary3.reset()
const retryResult = boundary3.render(() => {
if (shouldFail) throw new Error('Временный сбой API')
return 'Данные загружены'
})
console.log('После сброса — вывод:', retryResult.output) // 'Данные загружены'
console.log('Попыток:', boundary3.getState().retryCount) // 1
// --- Тест 4: Изолированные зоны (несколько boundary) ---
console.log('
=== Тест 4: Изолированные зоны ===')
const weatherBoundary = createErrorBoundary({ fallback: '[Ошибка погодного виджета]' })
const chatBoundary = createErrorBoundary({ fallback: '[Ошибка чата]' })
const weather = weatherBoundary.render(() => {
throw new Error('API погоды недоступен')
})
const chat = chatBoundary.render(() => 'Чат работает нормально')
console.log('Погода:', weather.output) // fallback
console.log('Чат:', chat.output) // нормально
console.log('Чат упал?', chatBoundary.getState().hasError) // false — изолировано!Представьте: пользователь открывает ваш интернет-магазин. В одном из компонентов списка товаров приходит null вместо объекта — и React, не зная как отрисовать null.price, бросает ошибку. Без Error Boundary весь экран становится белым. Пользователь видит пустую страницу и уходит.
Error Boundary — это React-компонент, который перехватывает ошибки рендеринга в дочерних компонентах и вместо белого экрана показывает запасной UI (fallback).
Error Boundary — это классовый компонент с двумя специальными методами жизненного цикла:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
// Вызывается при ошибке рендеринга — обновляет state
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
// Вызывается после getDerivedStateFromError — для логирования
componentDidCatch(error, errorInfo) {
console.error('Ошибка компонента:', error)
console.error('Стек компонентов:', errorInfo.componentStack)
// Отправляем в Sentry, Datadog, etc.
logErrorToService(error, errorInfo)
}
render() {
if (this.state.hasError) {
return <h2>Что-то пошло не так. Попробуйте обновить страницу.</h2>
}
return this.props.children
}
}
// Использование:
<ErrorBoundary>
<UserProfile userId={id} /> {/* если здесь ошибка — показывается fallback */}
</ErrorBoundary>| Метод | Когда вызывается | Для чего |
|---|---|---|
| getDerivedStateFromError | Во время рендеринга (фаза render) | Обновить state → показать fallback UI |
| componentDidCatch | После рендеринга (фаза commit) | Логировать ошибку, side effects |
Важно: getDerivedStateFromError — статический метод (static), у него нет доступа к this. Он должен быть чистой функцией.
Error Boundary перехватывает только ошибки рендеринга (метод render и lifecycle методы). Следующие ошибки он не ловит:
setTimeout, fetch, промисы — используйте try/catchonClick — используйте try/catch внутри// ОШИБКА: это НЕ поймает Error Boundary
function BrokenButton() {
const handleClick = () => {
throw new Error('Ошибка в обработчике') // не поймается!
}
return <button onClick={handleClick}>Нажми</button>
}
// ПРАВИЛЬНО: try/catch в обработчике
function FixedButton() {
const handleClick = () => {
try {
riskyOperation()
} catch (error) {
setError(error.message)
}
}
return <button onClick={handleClick}>Нажми</button>
}Писать классовый компонент каждый раз — неудобно. Библиотека react-error-boundary предоставляет готовое решение:
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Произошла ошибка:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Попробовать снова</button>
</div>
)
}
// Использование с кнопкой сброса:
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => logError(error, info)}
onReset={() => {
// сбросить состояние приложения перед повтором
}}
>
<UserDashboard />
</ErrorBoundary>Одна из лучших возможностей react-error-boundary — кнопка "Попробовать снова". При нажатии компонент размонтируется и монтируется заново:
import { useErrorBoundary } from 'react-error-boundary'
// Программный сброс из дочернего компонента:
function UserProfile() {
const { showBoundary } = useErrorBoundary()
async function fetchUser() {
try {
const data = await api.getUser()
setUser(data)
} catch (error) {
showBoundary(error) // пробрасываем ошибку в boundary
}
}
}Не нужно оборачивать каждый компонент. Подумайте, какие части UI независимы:
// Хорошо: изолируем виджеты
<ErrorBoundary fallback={<p>Ошибка виджета</p>}>
<WeatherWidget />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Ошибка чата</p>}>
<ChatWidget />
</ErrorBoundary>
// Если упадёт WeatherWidget — ChatWidget продолжит работатьОбщее правило: один Error Boundary на каждую независимую часть интерфейса.
Реализация паттерна Error Boundary через JavaScript: перехват ошибок, логирование, сброс состояния и изолированные зоны ошибок
// Демонстрируем концепцию Error Boundary на чистом JavaScript.
// В React это классовый компонент, здесь — функция-обёртка с тем же поведением.
// --- Симуляция компонентов ---
function simulateRender(componentName, props) {
// Некоторые компоненты "падают" при определённых данных
if (componentName === 'UserCard' && props.user === null) {
throw new Error('Cannot read property "name" of null')
}
if (componentName === 'PriceDisplay' && props.price < 0) {
throw new Error('Недопустимая цена: ' + props.price)
}
return '[rendered: ' + componentName + ']'
}
// --- Error Boundary: паттерн обёртки ---
function createErrorBoundary(options = {}) {
const { onError, fallback = 'Что-то пошло не так', resetOnRetry = true } = options
let state = {
hasError: false,
error: null,
retryCount: 0,
}
function getDerivedStateFromError(error) {
// Аналог статического метода в React
return { hasError: true, error }
}
function componentDidCatch(error, context) {
// Аналог метода жизненного цикла
console.error('[ErrorBoundary] Поймана ошибка:', error.message)
console.error('[ErrorBoundary] Контекст:', context)
if (onError) onError(error, context)
}
function render(renderFn, context) {
if (state.hasError) {
console.log('[ErrorBoundary] Показываем fallback: "' + fallback + '"')
console.log('[ErrorBoundary] Ошибка была:', state.error.message)
return { output: fallback, error: state.error }
}
try {
const output = renderFn()
return { output, error: null }
} catch (error) {
const newState = getDerivedStateFromError(error)
state = { ...state, ...newState }
componentDidCatch(error, context || 'неизвестный компонент')
// После обновления state — рендерим снова (теперь покажет fallback)
return render(renderFn, context)
}
}
function reset() {
state = { hasError: false, error: null, retryCount: state.retryCount + 1 }
console.log('[ErrorBoundary] Сброшен. Попытка #' + state.retryCount)
}
return { render, reset, getState: () => ({ ...state }) }
}
// --- Тест 1: Компонент падает ---
console.log('=== Тест 1: Компонент падает ===')
const boundary1 = createErrorBoundary({
fallback: 'Ошибка загрузки профиля',
onError: (err) => console.log('[Логирование] Отправка в Sentry:', err.message),
})
const result1 = boundary1.render(
() => simulateRender('UserCard', { user: null }),
'UserCard'
)
console.log('Вывод:', result1.output) // fallback
console.log('Состояние:', boundary1.getState())
// --- Тест 2: Компонент работает нормально ---
console.log('
=== Тест 2: Нормальная работа ===')
const boundary2 = createErrorBoundary({ fallback: 'Ошибка цены' })
const result2 = boundary2.render(
() => simulateRender('PriceDisplay', { price: 1999 }),
'PriceDisplay'
)
console.log('Вывод:', result2.output) // нормальный рендер
console.log('Есть ошибка:', boundary2.getState().hasError) // false
// --- Тест 3: Сброс и повтор ---
console.log('
=== Тест 3: Сброс после ошибки ===')
const boundary3 = createErrorBoundary({ fallback: 'Временная ошибка' })
let shouldFail = true
// Первый рендер — упадёт
boundary3.render(() => {
if (shouldFail) throw new Error('Временный сбой API')
return 'Данные загружены'
})
console.log('После ошибки — hasError:', boundary3.getState().hasError)
// "Исправляем" проблему и нажимаем Retry
shouldFail = false
boundary3.reset()
const retryResult = boundary3.render(() => {
if (shouldFail) throw new Error('Временный сбой API')
return 'Данные загружены'
})
console.log('После сброса — вывод:', retryResult.output) // 'Данные загружены'
console.log('Попыток:', boundary3.getState().retryCount) // 1
// --- Тест 4: Изолированные зоны (несколько boundary) ---
console.log('
=== Тест 4: Изолированные зоны ===')
const weatherBoundary = createErrorBoundary({ fallback: '[Ошибка погодного виджета]' })
const chatBoundary = createErrorBoundary({ fallback: '[Ошибка чата]' })
const weather = weatherBoundary.render(() => {
throw new Error('API погоды недоступен')
})
const chat = chatBoundary.render(() => 'Чат работает нормально')
console.log('Погода:', weather.output) // fallback
console.log('Чат:', chat.output) // нормально
console.log('Чат упал?', chatBoundary.getState().hasError) // false — изолировано!Создай Error Boundary компонент на классах, который перехватывает ошибки в дочерних компонентах. Добавь кнопку "Повторить" для сброса ошибки. Компонент BuggyCounter должен падать при достижении счётчика 5.
В execute: оберни вызов component(props) в try/catch. В catch увеличь errorCount++, сохрани error в lastError, вызови onError?.(error), верни { success: false, error, fallback }. В try верни { success: true, result }. getErrorCount просто возвращает errorCount.