Express: Обработка ошибок
Express имеет специальный тип middleware для обработки ошибок — функцию с сигнатурой (err, req, res, next), которая перехватывает все ошибки, переданные через next(err) или брошенные в async-обработчиках.
Зачем нужно
Без централизованной обработки ошибок каждый маршрут содержит дублирующий код try/catch и формирования ответа. Error middleware позволяет вынести логику обработки в одно место: логировать ошибки, формировать единый формат ответа, скрывать внутренние детали от клиента в production.
Где используется
- REST API — единый формат ошибок
{ error, message, statusCode } - Валидация входных данных (Joi, Zod) — перехват ValidationError
- Работа с БД — перехват ошибок уникальности, соединения
- Авторизация — UnauthorizedError, ForbiddenError
- Глобальный fallback — 500 Internal Server Error для непредвиденных ошибок
Основной контент
Базовый error middleware
// Регистрируется ПОСЛЕДНИМ, после всех маршрутов
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error'
});
});
Передача ошибки через next
app.get('/users/:id', (req, res, next) => {
UserService.getById(req.params.id)
.then(user => {
if (!user) {
const err = new Error('User not found');
err.status = 404;
return next(err);
}
res.json(user);
})
.catch(next); // передаём ошибку БД в error middleware
});
Кастомный класс ошибки
// errors/AppError.js
class AppError extends Error {
constructor(message, status = 500) {
super(message);
this.status = status;
this.name = this.constructor.name;
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
module.exports = { AppError, NotFoundError, ValidationError };
Обёртка для async-маршрутов
// utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Использование
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await UserService.getById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
}));
Полный error middleware
// middleware/errorHandler.js
const { AppError } = require('../errors/AppError');
module.exports = (err, req, res, next) => {
// Логирование
if (process.env.NODE_ENV !== 'test') {
console.error(`[${new Date.toISOString()}] ${err.name}: ${err.message}`);
}
// Известные ошибки приложения
if (err instanceof AppError) {
return res.status(err.status).json({
error: err.name,
message: err.message
});
}
// Ошибки Mongoose уникальности
if (err.code === 11000) {
return res.status(409).json({ error: 'Conflict', message: 'Duplicate entry' });
}
// Неизвестная ошибка — скрываем детали в production
res.status(500).json({
error: 'InternalServerError',
message: process.env.NODE_ENV === 'production' ? 'Something went wrong' : err.message
});
};
404 для несуществующих маршрутов
// Ставится перед error middleware, после всех маршрутов
app.use((req, res, next) => {
next(new NotFoundError(`Route ${req.method} ${req.path}`));
});
app.use(errorHandler); // error middleware последним
Частые ошибки
- 4 параметра обязательны — если написать
(err, req, res)безnext, Express не распознает как error middleware - Регистрировать error middleware до маршрутов — он должен быть последним
app.use - Не вызывать
nextпослеres.json— приведёт к "Cannot set headers after they are sent" - Забыть
catch(next)в Promise-цепочках — необработанные rejections не попадут в error middleware - Отдавать stack trace в production — утечка внутренних деталей, уязвимость безопасности