Главная / Статьи / Удаление шаблонного кода try/catch из маршрутов Express с asyncHandler

Удаление шаблонного кода try/catch из маршрутов Express с asyncHandler

Узнайте, как промежуточный компонент обработки ошибок, обертка asyncHandler и пользовательский класс AppError позволяют вынести ответы Express на ошибки из каждого маршрута в одно единое место.

1231 слов

Большинство API типа Express начинаются с блока try/catch в каждом асинхронном маршруте, который преобразует сбои в JSON-ответ. По мере роста API этот блок копируется в каждый обработчик. Здесь мы переместим ответы на ошибки в один промежуточный компонент, уберем повторяющийся блок try/catch с помощью небольшого обёртки и будем указывать коды состояния с помощью пользовательского класса ошибок. Что касается сбоев на уровне процесса, ознакомьтесь с нашим руководством по обработке ошибок производственного уровня в приложениях Node.js.

Почему подход try/catch для каждого маршрута неэффективен

Первая версия обычно оборачивает логику и отправляет код 500 при сбое.

app.get("/users", async (req, res) => {
  try {
    const users = await getUsers();
    res.json(users);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

Здесь всё в порядке, пока такая структура не повторяется десятками маршрутов.

app.get("/users", async (req, res) => {
  try {
    // ...
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

app.get("/products", async (req, res) => {
  try {
    // ...
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

app.post("/orders", async (req, res) => {
  try {
    // ...
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

Теперь каждый обработчик выполняет две задачи: реализацию бизнес-логики и определение вида отображения ошибок. Изменение формата означает необходимость редактирования каждого маршрута.

Мидлвэр для обработки ошибок в Express

Express распознаёт мидлвэр для обработки ошибок по количеству аргументов: функция с четырьмя параметрами, при этом ошибка указывается в первом параметре.

(err, req, res, next)

В минимальной версии происходит логирование и возвращается стандартный код ошибки 500. Его необходимо зарегистрировать после всех маршрутов, поскольку Express выполняет мидлвэры в определённом порядке. Сохраняйте все четыре параметра даже в случае, если функция next не используется — иначе Express будет рассматривать её как обычный мидлвэр.

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({
    message: "Internal Server Error"
  });
});

Передача ошибок с помощью next(err)

Вместо формирования ответа внутри блока catch маршрут передаёт ошибку в Express.

app.get("/users", async (req, res, next) => {
  try {
    const users = await getUsers();
    res.json(users);
  } catch (err) {
    next(err);
  }
});

Функция next(err) заставляет Express пропустить обычные мидлвэры и перейти к обработчику ошибок:

Request
   ↓
Route
   ↓
Business logic
   ↓
Error?
   ↓
next(err)
   ↓
Centralized error handler
   ↓
HTTP Response

Удаление оставшихся блоков try/catch

Каждый маршрут по-прежнему содержит блоки try/catch. Функция asyncHandler оборачивает функцию маршрута, запускает её с помощью Promise.resolve и передаёт любые ошибки в функцию next.

const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

Маршрут сводится к его фактической логике:

app.get(
  "/users",
  asyncHandler(async (req, res) => {
    const users = await getUsers();
    res.json(users);
  })
);

Когда функция getUsers() возвращает ошибку, обёртка вызывает:

next(err);

В Express 5 ошибки асинхронных операций автоматически передаются в функцию next, поэтому обёртка имеет значение в основном в Express 4.

Указание кодов состояния с помощью AppError

Ошибки становятся более полезными, когда они содержат код состояния HTTP:

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

Маршруты выбрасывают информативные ошибки вместо форматирования ответов:

app.get(
  "/users/:id",
  asyncHandler(async (req, res) => {
    const user = await getUser(req.params.id);
    if (!user) {
      throw new AppError("User not found", 404);
    }
    res.json(user);
  })
);

Обработчик считывает значение statusCode, при необходимости используя значение 500:

app.use((err, req, res, next) => {
  console.error(err);
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    success: false,
    message: err.message || "Internal Server Error"
  });
});

Отсутствие пользователя теперь приводит к коду 404:

{
  "success": false,
  "message": "User not found"
}

Внимание: воспроизведение значения err.message для каждой ошибки может привести к утечке внутренних данных, таких как сообщения базы данных. Возвращайте его только для экземпляров AppError, в остальных случаях используйте общий текст. Подробнее о стандартной структуре данных см. в нашем обзоре деталей проблемы RFC 9457.

Полная структура проекта

Простая структура:

src/
├── controllers/
│   └── user.controller.js
├── middleware/
│   ├── asyncHandler.js
│   └── errorHandler.js
├── errors/
│   └── AppError.js
├── routes/
│   └── user.routes.js
└── app.js

Модуль-обертка

const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

module.exports = asyncHandler;

Модуль класса ошибок

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

module.exports = AppError;

Модуль обработчика ошибок

const errorHandler = (err, req, res, next) => {
  console.error(err);
  res.status(err.statusCode || 500).json({
    success: false,
    message: err.message || "Internal Server Error"
  });
};
module.exports = errorHandler;

Контроллер, использующий оба компонента

Контроллеры загружают данные, при необходимости выбрасывают AppError, и возвращают данные в формате успеха.

const asyncHandler = require("../middleware/asyncHandler");
const AppError = require("../errors/AppError");

const getUsers = asyncHandler(async (req, res) => {
  const users = await userService.getUsers();
  res.json({
    success: true,
    data: users
  });
});

const getUser = asyncHandler(async (req, res) => {
  const user = await userService.getUser(req.params.id);
  if (!user) {
    throw new AppError("User not found", 404);
  }
  res.json({
    success: true,
    data: user
  });
});

module.exports = {
  getUsers,
  getUser
};

Подключение в файле app.js

Обработчик подключается в конце.

const express = require("express");
const userRoutes = require("./routes/user.routes");
const errorHandler = require("./middleware/errorHandler");

const app = express();
app.use(express.json());

app.use("/users", userRoutes);

// Must be after routes
app.use(errorHandler);

module.exports = app;

Что дает использование шаблона

Контроллеры больше не повторяются:

try {
  // ...
} catch (err) {
  res.status(500).json(...);
}

Клиенты могут полагаться на одну структуру:

{
  "success": false,
  "message": "User not found"
}

Задача контроллера становится простой:

Get data
↓
Validate
↓
Process
↓
Return result

Логирование, идентификаторы запросов и отслеживание ошибок находятся в одном промежуточном модуле.

Порядок важен: регистрируйте обработчик в конце

Он должен следовать за каждым маршрутизатором, которого он обрабатывает:

app.use("/users", userRoutes);
app.use("/products", productRoutes);

// Error handler LAST
app.use(errorHandler);

Если зарегистрирован раньше, он не увидит ошибки из более поздних маршрутов.

Итоги

Вместо того чтобы:

Route
 └── try/catch
      └── send error response

становится:

Route
 └── business logic
      └── throw error
           ↓
      asyncHandler
           ↓
      centralized error handler
           ↓
      consistent HTTP response

Настоящая цель — разделение бизнес-логики от отображения ошибок:

  • Промежуточный модуль для обработки ошибок требует четырех параметров и должен быть зарегистрирован в конце.
  • Используйте asyncHandler в Express 4; Express 5 сам обрабатывает отклонения асинхронных операций.
  • Вызывайте AppError с указанием статуса для ожидаемых ошибок и скрывайте сообщения о неожиданных.
  • Связанная литература