Необработанная ошибка во frontend — это белый экран: упал один компонент, рухнуло всё приложение. Это худший исход для пользователя, и он предотвратим. Ключ — понимать, что ошибки бывают двух родов, и у каждого свой механизм: сбой при рендере и ошибка при получении данных.
Два рода ошибок
Сбой рендера — исключение в коде компонента во время отрисовки (обращение к undefined, баг в логике). Такие ловит error boundary. Ошибка данных — запрос к серверу вернул ошибку; это не исключение рендера, а ожидаемое состояние, и обрабатывается оно состоянием, а не boundary. Путать их — частая причина, по которой обработка ошибок «не срабатывает».
Error boundary
Error 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>
Boundary ставят не один на всё приложение, а по зонам: рухнул виджет — страница жива (об этом ниже).
Ошибки данных
Ошибку запроса не ловит boundary — её отдаёт 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
Suspense — декларативная граница загрузки: вместо if (isPending) в каждом компоненте оборачиваешь поддерево и задаёшь общий fallback. Он же — механизм для lazy-загрузки компонентов.
<Suspense fallback={<Spinner />}>
<LazyDashboard />
</Suspense>
Suspense и error boundary дополняют друг друга: первый отвечает за «грузится», второй за «упало». Часто их ставят рядом, оборачивая одну зону.
Graceful degradation
Главная идея — изолировать сбои. Интерфейс делят на зоны, и каждую важную зону оборачивают своим error boundary, чтобы падение одной не уносило остальные.
<Page>
<ErrorBoundary fallback={<WidgetError />}><Recommendations /></ErrorBoundary>
<ErrorBoundary fallback={<WidgetError />}><RecentOrders /></ErrorBoundary>
</Page>
Упали рекомендации — заказы и навигация работают. Это и есть graceful degradation: продукт остаётся полезным, даже когда часть его сломалась. Осмысленный fallback (с возможностью повтора или хотя бы понятным сообщением) всегда лучше пустого экрана.
Где это в UCP
Обработка ошибок — часть «готово», не полировка: error boundary ловит сбои рендера по зонам, состояния Query — ошибки данных, Suspense — загрузку, а изоляция зон не даёт одному сбою уронить продукт. Это та же дисциплина «явные ошибки и предсказуемые отказы», что error handling в backend, только на стороне пользователя. Для продукт-инженера, ведущего продукт до пользователя, «не показать белый экран» — прямая ответственность; а сколько эта устойчивость стоит по производительности — следующая тема.