Необработанная ошибка во 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, только на стороне пользователя. Для продукт-инженера, ведущего продукт до пользователя, «не показать белый экран» — прямая ответственность; а сколько эта устойчивость стоит по производительности — следующая тема.