Middleware в Express: рабочие приёмы из практики
Порядок подключения middleware решает, поймаете вы ошибку или уроните сервер. Разбираем, где ставить обработчик ошибок, как не потерять async-исключения и чем платить за каждый подход.

Вы добавили app.use(errorHandler) в начало файла, а обработчик всё равно не срабатывает. Или логгер пишет тело запроса как undefined, хотя express.json() явно подключён. Обе проблемы — про порядок и природу middleware в Express. Разберём приёмы, которые снимают эти грабли на проектах, где маршрутов десятки, а не три.
Статья про Express 4.x и 5.x. Между ними есть разница в обработке async-ошибок — о ней отдельно ниже. Инвалидация сессий, настройка CORS и rate limiting — смежные темы, здесь их не трогаем: только сам конвейер middleware и типичные ошибки при его сборке.
Middleware — это конвейер, а не набор
Express выполняет middleware строго в порядке подключения. Каждая функция получает (req, res, next) и обязана либо завершить ответ (res.send, res.json), либо передать управление дальше через next(). Забыли next() и не ответили — запрос повиснет до таймаута клиента.
Это объясняет две трети «мистических» багов. Логгер не видит тело, потому что подключён раньше express.json() — на момент его вызова парсер ещё не заполнил req.body. Аутентификация не срабатывает на части маршрутов, потому что они объявлены выше в файле, чем app.use(auth).
Правило, которое экономит часы отладки: порядок app.use в файле — это и есть порядок выполнения. Читайте файл сверху вниз как список шагов, через которые проходит запрос.Базовый скелет: что за чем
Рабочий порядок для типичного API выглядит так:
const express = require('express');
const app = express();
// 1. Парсеры тела — до всего, что читает req.body
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 2. Сквозные middleware: логирование, request id
app.use((req, res, next) => {
req.requestId = crypto.randomUUID();
console.log(`[${req.requestId}] ${req.method} ${req.url}`);
next();
});
// 3. Маршруты
app.use('/api/users', usersRouter);
app.use('/api/orders', ordersRouter);
// 4. 404 — ловит всё, что не подошло ни одному маршруту
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// 5. Обработчик ошибок — всегда последним, 4 аргумента
app.use((err, req, res, next) => {
console.error(`[${req.requestId}] ${err.stack}`);
res.status(err.status || 500).json({ error: err.message });
});Обработчик ошибок обязан иметь ровно четыре параметра — (err, req, res, next). Express отличает его от обычного middleware именно по количеству аргументов через fn.length. Уберёте next из сигнатуры — Express перестанет считать функцию обработчиком ошибок, и она не сработает. Это не стилистика, а условие распознавания.
Async-ошибки: где Express 4 молчит
Главная ловушка. В Express 4.x исключение внутри async-функции не попадает в обработчик ошибок автоматически:
// Express 4.x — так делать нельзя
app.get('/user/:id', async (req, res) => {
const user = await db.findUser(req.params.id); // если бросит — необработанный reject
res.json(user);
});Отклонённый промис здесь не превратится в вызов next(err). Клиент повиснет, а в консоли вы увидите UnhandledPromiseRejection. Три способа это лечить:
- Ручной try/catch с next(err) — многословно, но прозрачно. Подходит, когда обёрток не хочется.
- Обёртка-хелпер — одна функция, которая ловит reject и вызывает
next. Меньше всего кода на маршрут. - Переход на Express 5.x — там reject из async-обработчика автоматически идёт в обработчик ошибок. Проблема исчезает на уровне фреймворка.
Обёртка asyncHandler
// Работает в Express 4.x и 5.x
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/user/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) {
const err = new Error('User not found');
err.status = 404;
throw err; // попадёт в catch, оттуда в next(err), оттуда в обработчик ошибок
}
res.json(user);
}));Тот же приём есть в готовом виде — пакет express-async-errors патчит роутер и делает обёртку ненужной. Ставить его или писать четыре строки самому — вопрос вкуса и того, сколько внешних зависимостей вы готовы держать.
Сравнение подходов к async-ошибкам
| Подход | Кода на маршрут | Что выигрываете | Чем платите |
|---|---|---|---|
| try/catch вручную | 4–6 строк | Полный контроль, нулевая магия | Дублирование, легко забыть в одном месте |
| asyncHandler своя | 1 обёртка на маршрут | Явно видно в коде, без зависимостей | Надо не забыть обернуть каждый async-маршрут |
| express-async-errors | 0 | Работает прозрачно для всех маршрутов | Внешняя зависимость, «неявная» магия при чтении кода |
| Express 5.x | 0 | Решено фреймворком, обёртки не нужны | Требует миграции с 4.x, часть middleware может сломаться |
Middleware для группы маршрутов
Не всё должно висеть глобально через app.use. Аутентификацию логично включать только там, где она нужна. Три уровня применения:
- Глобально:
app.use(logger)— на каждый запрос. - На роутер:
app.use('/api/admin', adminRouter)плюсadminRouter.use(requireAdmin)— на все маршруты группы. - На один маршрут:
app.get('/profile', requireAuth, handler)— middleware передаётся аргументом перед обработчиком.
Последний вариант читается лучше всего: глядя на строку маршрута, вы сразу видите его защиту. Цена — если защита нужна на десяти маршрутах, вы повторите requireAuth десять раз. Тогда вынос на уровень роутера чище.
Порядок при монтировании роутеров
Middleware, подключённое на роутер через router.use(), действует только на маршруты, объявленные после него внутри этого роутера. И только на путях, начинающихся с префикса монтирования. Частая ошибка — объявить публичный маршрут ниже router.use(requireAuth) и удивляться, почему он требует токен.
Когда что выбирать
- Новый проект на Express 5.x — не пишите обёрток для async, полагайтесь на встроенную обработку reject.
- Существующий проект на 4.x, миграция не планируется — заведите один
asyncHandlerи оберните им все async-маршруты. Правило простое: async-функция без обёртки в коде — это баг на ревью. - Middleware нужно на всех маршрутах без исключений (логирование, request id) — глобальный
app.useв начале. - Защита нужна на подмножестве — уровень роутера или отдельного маршрута, глобально не вешать.
Что ломается на практике
- Обработчик ошибок не последний. Если после него объявлен ещё один
app.useс маршрутом, ошибки из того маршрута обработаются, а из объявленных выше — нет. Порядок жёсткий: 404, потом error handler, дальше ничего. - Двойной ответ. Middleware вызвало и
res.json(), иnext(). Второй обработчик пытается писать в уже закрытый ответ — получаетеERR_HTTP_HEADERS_SENT. Правило: либо ответ, либоnext(), никогда оба. - Тело пустое в логгере. Логгер подключён раньше
express.json(). Парсеры тела — всегда в самом верху. - Синхронный throw в Express 4. Внутри обычного (не async) middleware Express 4 ловит синхронный
throwи передаёт в обработчик ошибок. А reject из промиса — нет. Это несимметрично и сбивает с толку.
Что делать дальше
Откройте главный файл приложения и прочитайте все app.use сверху вниз как маршрут запроса. Проверьте три вещи: парсеры тела вверху, обработчик ошибок с четырьмя аргументами в самом низу, ни одного async-маршрута без обёртки (если вы на 4.x). В проде смотрите на счётчик необработанных reject и на запросы, зависшие до таймаута, — это прямые симптомы потерянных ошибок в конвейере. Дальше стоит разобраться с централизованным форматом ошибок: единый класс AppError со статусом и кодом упрощает и обработчик, и клиента.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Express: Using middleware (официальная документация), Express: Error handling
Частые вопросы
У меня Express 5, а не 4 — нужна ли обёртка asyncHandler?
Почему мой обработчик ошибок не срабатывает, хотя подключён?
Middleware выполняется не в том порядке, что я ожидаю. В чём дело?
Получаю ERR_HTTP_HEADERS_SENT — что не так?
Стоит ли ставить express-async-errors или писать обёртку самому?
Куда подключать middleware — глобально или на маршрут?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.