Удаление шаблонного кода try/catch из маршрутов Express с asyncHandler
Узнайте, как промежуточный компонент обработки ошибок, обертка asyncHandler и пользовательский класс AppError позволяют вынести ответы Express на ошибки из каждого маршрута в одно единое место.
Большинство 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 с указанием статуса для ожидаемых ошибок и скрывайте сообщения о неожиданных.Связанная литература
- Сбалансирование генерации CRUD-маршрутов Prisma с целенаправленным управлением маршрутами — Узнайте, как генерация CRUD-маршрутов Prisma на основе схемы позволяет избавиться от повторяющегося шаблонного кода, сохраняя при этом решения по надежности, ограничению доступа и открытию функций в коде приложения.
- Проектирование API на Node.js с использованием слоев: от громоздких контроллеров к чистой архитектуре — Узнайте, как переписать API на Node.js с использованием слоев контроллеров, сервисов и доступа к данным для устранения запутанной бизнес-логики, неоднородных ошибок и проблем с масштабированием.
- Отправка и прием SMS в Node.js с использованием Twilio Webhooks и Express — Создайте небольшой сервер на Express, который отправляет текстовые сообщения через Twilio, принимает ответы по webhookу, доступному через ngrok, и автоматически отвечает на них с помощью TwiML.
- Обрезка строк с учётом эмодзи в JavaScript с использованием Intl.Segmenter — почему метод slice() и обрезка с использованием распространения приводят к повреждению эмодзи и текста с диакритическими знаками, как из-за этого ломались реальные продукты, и как вместо этого обрезать строки по границам графем.
- Проверка входных данных на сервере в Express с использованием цепочек express-validator — узнайте, почему API Express должны проверять входные данные на сервере, как цепочки express-validator фиксируют ошибки для validationResult, и как структурировать маршрут в производственной среде.