← назад к разделу

Вы открываете страницу, и вместо контента — пустой белый экран. Приложение упало. Чаще всего причина — одна небольшая ошибка в одном месте, которая утащила за собой всё остальное. Это предотвратимо.

Разберём с нуля: почему так происходит, какие бывают виды ошибок во 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 и откуда берутся сбои рендера.