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

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


Основные принципы работы action

action вызывается, когда форма, связанная с маршрутом, отправляется. Он получает объект request, содержащий все данные запроса, и объект params, где находятся параметры маршрута.

Простейший пример action:

export async function action({ request, params }) {
  const formData = await request.formData();
  const username = formData.get("username");
  
  if (!username) {
    throw new Response("Имя пользователя обязательно", { status: 400 });
  }
  
  // логика сохранения данных
  return redirect(`/profile/${username}`);
}

Ключевой момент: для обработки ошибок используется выброс экземпляров Response с соответствующим статусом. Это позволяет маршрутам с ErrorBoundary корректно отлавливать ошибки.


Использование ErrorBoundary для маршрутов

Каждый маршрут в React Router может иметь собственный errorElement. Если action выбрасывает исключение или Response с кодом ошибки, компонент errorElement рендерится вместо обычного компонента маршрута.

<Route
  path="profile"
  element={<Profile />}
  action={profileAction}
  errorElement={<ProfileError />}
/>

Внутри ProfileError можно получить объект ошибки через хук useRouteError:

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

function ProfileError() {
  const error = useRouteError();

  return (
    <div>
      <h2>Произошла ошибка</h2>
      <p>{error.statusText || error.message}</p>
    </div>
  );
}

Важные моменты:

  • error.status и error.statusText доступны только если ошибка была выброшена как Response.
  • Для обычных исключений (throw new Error("...")) можно использовать error.message.

Асинхронная обработка и ошибки

action часто выполняет асинхронные операции: запросы к API, запись в базу данных, проверки. Для безопасной обработки ошибок используется блок try/catch:

export async function action({ request }) {
  try {
    const formData = await request.formData();
    const email = formData.get("email");

    const response = await fetch("/api/subscribe", {
      method: "POST",
      body: JSON.stringify({ email }),
    });

    if (!response.ok) {
      throw new Response("Ошибка сервера при подписке", { status: response.status });
    }

    return redirect("/thank-you");
  } catch (err) {
    if (err instanceof Response) {
      throw err; // пробрасываем Response для ErrorBoundary
    }
    throw new Response("Непредвиденная ошибка", { status: 500 });
  }
}

Такой подход гарантирует, что все ошибки попадут в ErrorBoundary, а пользователь увидит понятное сообщение.


Валидация данных и ошибки формы

React Router не ограничивается простым выбросом ошибок — он поддерживает возврат ошибок в форме без редиректа:

export async function action({ request }) {
  const formData = await request.formData();
  const password = formData.get("password");

  if (password.length < 6) {
    return { errors: { password: "Пароль должен быть не менее 6 символов" } };
  }

  return redirect("/dashboard");
}

В компоненте формы можно использовать useActionData для отображения ошибок:

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

function LoginForm() {
  const actionData = useActionData();

  return (
    <form method="post">
      <input type="password" name="password" />
      {actionData?.errors?.password && <p>{actionData.errors.password}</p>}
      <button type="submit">Войти</button>
    </form>
  );
}

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

  • return { errors: {...} } не вызывает ErrorBoundary.
  • Подходит для ошибок валидации и пользовательских уведомлений.

Принципы структурирования ошибок

  1. Использование Response для системных ошибок: сервер недоступен, база данных не отвечает, ошибка авторизации.
  2. Возврат объектов с errors для ошибок формы: поля не заполнены, некорректные данные.
  3. Разделение логики: одна функция action не должна одновременно обрабатывать системные ошибки и ошибки валидации — лучше делегировать в отдельные утилиты.

Советы по организации кода

  • Для крупных приложений полезно создавать универсальный обработчик ошибок, который преобразует исключения в Response или объект с errors.
  • Все асинхронные операции внутри action должны обрабатываться через try/catch, чтобы не возникали необработанные промисы.
  • Для маршрутов с общими формами и action лучше использовать типовую структуру возврата ошибок, чтобы унифицировать обработку на фронтенде.

Пример комплексного action с разными типами ошибок

export async function action({ request }) {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");

  const errors = {};
  if (!email.includes("@")) errors.email = "Неверный формат email";
  if (password.length < 6) errors.password = "Пароль слишком короткий";

  if (Object.keys(errors).length > 0) return { errors };

  try {
    const response = await fetch("/api/register", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) throw new Response("Ошибка сервера", { status: response.status });

    return redirect("/welcome");
  } catch (err) {
    if (err instanceof Response) throw err;
    throw new Response("Неизвестная ошибка", { status: 500 });
  }
}

Такой подход позволяет одновременно обрабатывать:

  • ошибки валидации формы без редиректа,
  • серверные ошибки через ErrorBoundary,
  • непредвиденные ошибки с кодом 500.

Хотите, я могу подготовить отдельную схему с визуальным потоком данных ошибок в action, чтобы её можно было вставить прямо в учебник и наглядно показать взаимодействие action, useActionData и ErrorBoundary?