Как обрабатывать ошибки на уровне компонентов? Как отлавливать ошибки в рендере?

Обработка ошибок на уровне компонентов в React — это критически важная часть разработки устойчивых интерфейсов. Ошибки могут возникнуть в рендеринге, методах жизненного цикла, асинхронных вызовах, событиях, хуках и других частях UI. React предоставляет встроенные и расширяемые способы отлавливать, логировать и реагировать на такие ошибки, чтобы не приводить к падению всего приложения.

1. Ошибки в рендеринге и жизненном цикле

С версии React 16 появилась концепция Error Boundaries (граничные обработчики ошибок). Они перехватывают ошибки:

  • в render()

  • в constructor()

  • в componentDidMount(), componentDidUpdate(), componentWillUnmount()

  • в методах дочерних компонентов

Они не ловят ошибки:

  • в обработчиках событий (onClick, и т.п.)

  • в асинхронных функциях (setTimeout, fetch, Promise)

  • в серверном рендеринге (SSR)

  • в хуках (useEffect, useMemo, и т.п.)

2. Реализация Error Boundary

Для использования Error Boundary необходимо создать класс-компонент, реализующий методы componentDidCatch и getDerivedStateFromError.

import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error }; // обновит состояние
}
componentDidCatch(error, errorInfo) {
console.error('Ошибка в компоненте:', error, errorInfo);
// можно отправить лог на сервер
}
render() {
if (this.state.hasError) {
return <h2>Что-то пошло не так.</h2>;
}
return this.props.children;
}
}

Применение:

<ErrorBoundary>
<ComponentWithRiskyRender />
</ErrorBoundary>

Можно оборачивать как всё приложение, так и отдельные части (например, каждая вкладка, карточка или виджет).

3. Использование нескольких Error Boundaries

Можно создать разные обработчики для разных частей UI, чтобы ошибка в одной области не приводила к краху всего приложения:

<Header />
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
<ErrorBoundary>
<Notifications />
</ErrorBoundary>
<Footer />

Такой подход позволяет изолировать ошибки и показывать локальные fallback UI.

4. Error Boundary с кастомным интерфейсом

class CustomErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
// логирование
}
render() {
const { error } = this.state;
if (error) {
return (
<div className="error-ui">
<h1>Ошибка</h1>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Перезагрузить</button>
</div>
);
}
return this.props.children;
}
}

5. Ошибки в хуках и функциональных компонентах

React не позволяет использовать componentDidCatch в функциональных компонентах напрямую. Но с помощью библиотеки react-error-boundary можно реализовать Error Boundary и в функциональном стиле.

Пример с react-error-boundary:

npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
function FallbackComponent({ error, resetErrorBoundary }) {
return (
<div>
<p>Ошибка: {error.message}</p>
<button onClick={resetErrorBoundary}>Попробовать снова</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={FallbackComponent}
onReset={() => {
// сбросить состояние или выполнить перенаправление
}}
\>
&lt;RiskyComponent /&gt;
&lt;/ErrorBoundary&gt;
);
}

6. Ошибки в событиях и Promise

Ошибки внутри событий (например, onClick) не перехватываются Error Boundary. Их нужно ловить вручную через try/catch.

function handleClick() {
try {
// рискованная логика
} catch (error) {
console.error('Ошибка в обработчике события:', error);
}
}

То же касается fetch и других Promise:

async function loadData() {
try {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
} catch (error) {
console.error('Ошибка загрузки:', error);
setError(error);
}
}

7. Обработка ошибок в хуках (useEffect, useCallback, useMemo)

Асинхронные функции внутри хуков нужно защищать try/catch:

useEffect(() => {
const load = async () => {
try {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
} catch (e) {
setError(e);
}
};
load();
}, \[\]);

Также можно использовать кастомный хук useSafeAsync или библиотеку типа SWR, React Query — они уже имеют встроенные механизмы обработки ошибок.

8. Логирование ошибок

Ошибки полезно не только отображать, но и логировать на сервер:

componentDidCatch(error, errorInfo) {
sendToMonitoringService({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack
});
}

Можно интегрировать такие сервисы, как:

  • Sentry

  • Bugsnag

  • LogRocket

  • Datadog

  • Rollbar

Они позволяют собирать подробные отчёты об ошибках, включая состояние компонентов, контекст, действия пользователя и пр.

9. Условный рендеринг и защита от невалидных данных

Иногда ошибки вызваны не исключениями, а невалидными данными (например, undefined, null и т.п.). В таких случаях стоит использовать защитные проверки:

if (!user) return &lt;Loading /&gt;;
return &lt;UserProfile data={user} /&gt;;

Или с помощью опциональной цепочки:

&lt;p&gt;{user?.name}&lt;/p&gt;

10. Ошибки при SSR и Hydration

При серверной генерации (Next.js, Remix) ошибки могут возникнуть до загрузки JS. Для этого часто используют getInitialProps, getServerSideProps и другие серверные обработчики, где нужно обязательно отлавливать ошибки.

Также важно отлавливать ошибки при hydrate() — если markup отличается между сервером и клиентом, React выдаст предупреждение.

11. Интеграция с системами трекинга ошибок

Библиотеки типа Sentry могут оборачивать Error Boundaries:

import \* as Sentry from '@sentry/react';
const SentryBoundary = Sentry.ErrorBoundary;
&lt;SentryBoundary fallback={<p&gt;Произошла ошибка&lt;/p&gt;}>
&lt;ComponentWithRisk /&gt;
&lt;/SentryBoundary&gt;

Также они могут автоматически перехватывать ошибки из window.onerror, unhandledrejection и других глобальных источников.

Грамотное управление ошибками на уровне компонентов — это не только Error Boundaries, но и осознанное проектирование: валидация данных, защита асинхронных операций, логирование и fallback UI. Чем изолированнее и предсказуемее ведёт себя каждый компонент при сбое, тем стабильнее работает всё приложение.