Обработка ошибок в loader

loader в React Router — это функция, которая асинхронно загружает данные перед рендерингом компонента маршрута. Она позволяет реализовать data fetching на уровне маршрутов и возвращать данные напрямую в компонент через хук useLoaderData. Однако при работе с внешними API, базами данных или другими асинхронными источниками данных всегда существует вероятность возникновения ошибок. React Router предоставляет встроенные механизмы для их обработки.


Использование throw для генерации ошибок

Внутри loader можно использовать throw для передачи ошибок маршруту:

import { json } from "react-router-dom";

export async function loader({ params }) {
  try {
    const response = await fetch(`/api/items/${params.id}`);
    if (!response.ok) {
      throw json({ message: "Элемент не найден" }, { status: 404 });
    }
    return response.json();
  } catch (error) {
    throw json({ message: error.message }, { status: 500 });
  }
}

Ключевые моменты:

  • throw json(data, options) — встроенный метод React Router для передачи ошибки с HTTP-статусом.
  • status используется для корректного отображения ошибки на клиенте и возможности перехвата её через errorElement.

errorElement и обработка ошибок на уровне маршрута

Каждый маршрут может иметь свой errorElement — компонент, который рендерится при возникновении ошибки в loader или в компоненте маршрута:

import { useRouteError } from "react-router-dom";

function ItemError() {
  const error = useRouteError();
  return (
    <div>
      <h2>Ошибка загрузки элемента</h2>
      <p>{error.status}: {error.data?.message || error.message}</p>
    </div>
  );
}

const routes = [
  {
    path: "/items/:id",
    element: <ItemPage />,
    loader: loader,
    errorElement: <ItemError />,
  }
];

Особенности:

  • useRouteError() возвращает объект ошибки, брошенной в loader или компоненте.
  • Объект ошибки содержит status и data (если используется throw json) или message для стандартных исключений.
  • Позволяет создавать пользовательские страницы ошибок для каждого маршрута.

Асинхронные ошибки и fallback

loader может возвращать промисы, которые могут быть отклонены. React Router корректно обрабатывает такие случаи и передает отклонённую ошибку в errorElement. Пример с асинхронной функцией:

export async function loader({ params }) {
  const response = await fetch(`/api/items/${params.id}`);
  if (!response.ok) {
    throw new Response("Not Found", { status: 404 });
  }
  return response.json();
}

Здесь throw new Response создаёт нативный объект ответа, который React Router автоматически распознает и передаст в useRouteError().


Обработка ошибок с помощью defer

Для маршрутов с defer данные загружаются лениво, и ошибки могут возникать на отдельных промисах. Их можно перехватывать через Await:

import { defer, Await, useLoaderData } from "react-router-dom";

export function loader() {
  return defer({
    item: fetchItem().catch(err => { throw err; })
  });
}

function ItemPage() {
  const data = useLoaderData();
  return (
    <Suspense fallback={<div>Загрузка...</div>}>
      <Await resolve={data.item} errorElement={<div>Ошибка при загрузке</div>}>
        {(item) => <div>{item.name}</div>}
      </Await>
    </Suspense>
  );
}

Ключевые моменты:

  • defer позволяет рендерить компонент до завершения загрузки всех данных.
  • Await обеспечивает обработку ошибок конкретного промиса отдельно.
  • errorElement в Await работает аналогично маршруту.

Перехват ошибок глобально

Для обработки всех ошибок приложения можно использовать корневой маршрут с errorElement. Это позволяет централизованно показывать сообщения о сбоях:

function RootError() {
  const error = useRouteError();
  return (
    <div>
      <h1>Произошла ошибка</h1>
      <p>{error.status || 500}: {error.statusText || error.message}</p>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <RootError />,
    children: [
      { path: "items/:id", element: <ItemPage />, loader: loader },
    ]
  }
]);

Преимущество глобальной обработки — единый интерфейс для ошибок, не зависящий от конкретного маршрута, с возможностью кастомизации вида страницы.


Практические советы

  1. Всегда использовать throw json или throw Response для ошибок, которые должны отображаться пользователю.
  2. Использовать errorElement на уровне маршрута для изоляции ошибок конкретного маршрута.
  3. Не забывать об асинхронных промисах при работе с defer, оборачивая возможные ошибки в catch или throw.
  4. Передавать полезные данные ошибки (например, message, status) для информативного отображения.

Эти подходы делают загрузку данных через loader безопасной и позволяют создавать устойчивый пользовательский интерфейс, корректно реагирующий на ошибки API или непредвиденные сбои при асинхронных запросах.