Вы открываете страницу, и вместо контента — пустой белый экран. Приложение упало. Чаще всего причина — одна небольшая ошибка в одном месте, которая утащила за собой всё остальное. Это предотвратимо.
Разберём с нуля: почему так происходит, какие бывают виды ошибок во frontend, и как правильно их перехватывать.
Почему одна ошибка роняет всё приложение
React рендерит компоненты по дереву. Если где-то в этом дереве выбрасывается исключение — например, пришли данные без нужного поля, и код пытается прочитать undefined.name — React не знает, что делать, и по умолчанию размонтирует всё дерево целиком. Пользователь видит белый экран.
Решение — явно указать React: «если в этом месте что-то упало, покажи запасной экран, но не трогай остальное». Для этого существует error boundary.
Ошибки бывают двух видов
Прежде чем выбирать инструмент, важно понять природу ошибки:
Сбой рендера — исключение прямо в коде компонента: обратились к undefined, сломалась логика вычисления. Такие ошибки перехватывает error boundary.
Ошибка данных — запрос к серверу вернул ошибку (сеть упала, сервер ответил 500). Это не исключение рендера — это ожидаемое состояние, которое обрабатывается отдельно, через состояние компонента.
Путать их — частая причина, по которой «обработка ошибок не работает»: поставили boundary, ждёте, что он поймает ошибку запроса, — но он её не поймает.
Error boundary: ловим сбои рендера
Error boundary — это компонент-обёртка, который перехватывает исключения в своём поддереве и показывает запасной экран вместо падения. React пока не добавил хук для этого, поэтому boundary пишут классовым компонентом — или берут готовую библиотеку react-error-boundary.
Минимальная реализация:
class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: unknown) {
// здесь можно отправить в систему мониторинга
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
Использование:
<ErrorBoundary fallback={<ErrorScreen />}>
<Dashboard />
</ErrorBoundary>
Если Dashboard или любой компонент внутри выбросит исключение, пользователь увидит <ErrorScreen />, а не белый экран.
Ошибки данных: состояния вместо boundary
Ошибку HTTP-запроса boundary не перехватит — она не исключение рендера. Правильный подход: запрос возвращает состояние isError, и компонент явно его обрабатывает.
Если вы используете TanStack Query, это выглядит так:
function Products() {
const { data, isPending, isError, refetch } = useProducts();
if (isPending) return <Spinner />;
if (isError) return <ErrorBox onRetry={() => refetch()} />;
return <ProductList products={data} />;
}
Ключевое: ошибка сети — это не катастрофа, а ожидаемое состояние. Сеть моргнула — показали сообщение и кнопку «повторить». Пользователь понимает, что происходит, и может что-то сделать.
Suspense: declarative loading-состояние
Suspense решает другую проблему — показ загрузки. Вместо того чтобы писать if (isPending) return <Spinner /> в каждом компоненте, вы оборачиваете поддерево и задаёте один общий fallback:
<Suspense fallback={<Spinner />}>
<LazyDashboard />
</Suspense>
Пока компоненты внутри «приостановлены» (загружаются), Suspense показывает fallback. Это же работает для ленивой загрузки компонентов через React.lazy.
Suspense и error boundary часто ставят рядом — они дополняют друг друга: первый отвечает за «грузится», второй за «упало».
<ErrorBoundary fallback={<WidgetError />}>
<Suspense fallback={<Spinner />}>
<LazyWidget />
</Suspense>
</ErrorBoundary>
Изоляция по зонам: не один boundary на всё
Ставить один error boundary на корень приложения — недостаточно. Если он сработает, пользователь потеряет весь интерфейс.
Правильнее разбить экран на независимые зоны и обернуть каждую важную зону своим boundary. Упала одна зона — остальные продолжают работать.
<Page>
<ErrorBoundary fallback={<WidgetError name="Рекомендации" />}>
<Recommendations />
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError name="Последние заказы" />}>
<RecentOrders />
</ErrorBoundary>
</Page>
Рекомендации упали — заказы и навигация живут. Пользователь продолжает работать с продуктом. Это и называют graceful degradation: продукт остаётся полезным, даже когда часть его сломалась.
Хороший fallback — не просто «что-то пошло не так», а понятное сообщение с возможностью попробовать ещё раз.
Коротко
- Необработанное исключение в компоненте по умолчанию роняет всё дерево React — пользователь видит белый экран.
- Ошибки бывают двух видов: сбой рендера (ловит error boundary) и ошибка данных (обрабатывается состоянием).
- Error boundary — компонент-обёртка с
getDerivedStateFromError, перехватывает исключения в поддереве и показывает запасной экран. - Ошибку HTTP-запроса boundary не поймает — обрабатывайте через состояние (
isError) с кнопкой повтора. - Suspense — декларативная граница загрузки; часто ставится рядом с boundary (один за «грузится», другой за «упало»).
- Boundary ставят по зонам, не один на всё приложение — чтобы падение виджета не уносило страницу целиком.
- Хороший fallback содержит понятное сообщение и возможность повторить действие.
Что почитать дальше
- Загрузка данных: TanStack Query — состояния
isPending/isErrorи мутации. - Производительность и оптимизация — ленивая загрузка компонентов с
React.lazy+Suspense. - Компонентный подход и состояние — как устроено дерево React и откуда берутся сбои рендера.