Обработка ошибок в Express: middleware ошибок
Содержание
О проверке кода. Примеры не запускались в этой сессии (по договорённости — фокус на контенте, а не прогонах). Говорим прямо, а не ставим бейдж «проверено».
Ошибки в API неизбежны: не тот ввод, недоступная БД, баг. Вопрос — как Express на них реагирует. Хаотичный try/catch в каждом маршруте — плохо; централизованный обработчик — правильно. Разберём механизм.
Особое middleware с четырьмя аргументами
Express распознаёт обработчик ошибок по числу параметров — их четыре, а не три:
// обычное middleware — 3 аргумента
app.use((req, res, next) => { /* ... */ });
// обработчик ОШИБОК — 4 аргумента, первый err
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Что-то пошло не так' });
});
Именно по четырём аргументам (err, req, res, next) Express понимает, что это обработчик ошибок, и вызывает его только при ошибке. Ставят его в самом конце, после всех маршрутов и middleware — тогда он ловит ошибки со всех них в одном месте. Это заменяет разбросанный по маршрутам try/catch единой точкой.
- 1 В маршруте ошибкаБД недоступна, throw, next(err)
- 2 Express пропускает обычные middlewareИщет обработчик с 4 аргументами
- 3 Обработчик ошибок (err, req, res, next)Логирует, формирует ответ
- 4 Единый ответ клиентуСтатус + понятное сообщение
Передача ошибки: next(err)
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.users.findById(req.params.id);
if (!user) {
return next(new NotFoundError('Пользователь не найден')); // передаём в обработчик
}
res.json(user);
} catch (err) {
next(err); // ошибку БД — тоже в обработчик
}
});
next(err) с аргументом-ошибкой перескакивает сразу к обработчику ошибок, минуя остальные обычные middleware. Это способ сказать «здесь проблема, разберись централизованно». В синхронном коде можно и просто throw — Express поймает сам. Но в асинхронном есть нюанс.
Ловушка async-ошибок
// ❌ Express 4: ошибка в async НЕ попадёт в обработчик автоматически
app.get('/users', async (req, res) => {
const users = await db.query(); // если бросит — повиснет, обработчик не вызовется
res.json(users);
});
// ✅ Express 4: ловим и передаём вручную
app.get('/users', async (req, res, next) => {
try {
const users = await db.query();
res.json(users);
} catch (err) {
next(err); // обязательно, иначе ошибка потеряется
}
});
В Express 4 ошибка внутри async-функции не подхватывается автоматически — отклонённый промис не доходит до обработчика, запрос зависает. Её ловят try/catch и передают через next(err). Чтобы не оборачивать каждый маршрут, используют обёртку:
// обёртка: ловит async-ошибки и сама зовёт next
const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users', asyncH(async (req, res) => {
const users = await db.query(); // ошибка сама уйдёт в обработчик
res.json(users);
}));
В Express 5 это исправлено — отклонённый промис уходит в обработчик ошибок автоматически, обёртка не нужна. Но проектов на Express 4 ещё много, поэтому знать про ловушку важно.
Свои классы ошибок
Чтобы обработчик знал, какой код вернуть, ошибкам дают тип и статус:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
class NotFoundError extends AppError {
constructor(msg = 'Не найдено') { super(msg, 404); }
}
class ValidationError extends AppError {
constructor(msg) { super(msg, 400); }
}
Свои классы ошибок несут статус-код, и обработчик использует его вместо жёсткого 500:
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
logger.error(err); // полный стек — в лог
res.status(status).json({
error: err.message || 'Внутренняя ошибка',
});
});
Теперь next(new NotFoundError()) даёт клиенту 404, а new ValidationError() — 400, автоматически и единообразно.
Не показывайте детали клиенту
// ❌ утечка внутренностей
res.status(500).json({ error: err.stack }); // стек виден клиенту!
// ✅ клиенту — общее, стек — в лог
app.use((err, req, res, next) => {
logger.error(err); // полный стек и контекст — на сервере
const isProd = process.env.NODE_ENV === 'production';
res.status(err.statusCode || 500).json({
error: isProd ? 'Внутренняя ошибка' : err.message,
});
});
В продакшене клиенту отдают общее сообщение, а полный стек логируют. Показывать стек и внутренние детали наружу — утечка информации о структуре приложения, помогающая злоумышленнику. В разработке детали полезны, поэтому режим переключают по NODE_ENV. Ошибки же логируют всегда — иначе о сбоях никто не узнает.
Практический чек-лист
- один обработчик ошибок с 4 аргументами — в самом конце;
- async-ошибки:
try/catch+next(err)или обёртка (Express 4); - свои классы ошибок со статус-кодами — единообразные ответы;
- клиенту — общее сообщение, в лог — полный стек;
- различайте операционные ошибки и баги — как и в чистом Node;
- всегда возвращайте корректный HTTP-статус.
Смежные темы: middleware — обработчик ошибок это особое middleware; обработка ошибок в Node; классы ошибок; REST API и статусы. Полный список — в уроках Node.js.