Адыяктуванне маршрутаў Express без неабходных кадэнтай try/catch за дапамою asyncHandler
Дазвольце дазнаць, як мідлвэр для адзначэння памылак, кэпсула asyncHandler і спецыяльны клас AppError дапамагаюць вынесці адпаведзі Express пра памылкі з кожнага маршруту і разместіць іх у адны, аднородны месца.
Крыніцы: Stop Repeating try/catch: A Cleaner Error Handling Pattern in Express.js
Большасць Express API-ў пачынаюцца з 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
Адна адзінастая пункт для стазіроўкі
Логаванне, ID запытак і стазіроўка адзінакоў знаходзяцца ў одным мідлвэры.
Порядак мае значэнне: рэгіструйце обробнік апошнім
Ён должен следаваць за кожным рутэрам, які ён павінен обрабляць:
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 5 сам выконвае обробку асінхронных адмоваў.AppError з адпаведным статусам, а паведамленні пра нечаканыя адмовы — хаваць.