Express обрабатывает ошибки специальным middleware, который принимает четыре аргумента, (err, req, res, next), и регистрируется после всех ваших маршрутов. Express передаёт ему каждую ошибку, выброшенную внутри маршрута, каждую ошибку, переданную в next(err), а в Express 5 ещё и каждый отклонённый промис из async-обработчика. Обработчик выбирает код статуса и отправляет один ответ с ошибкой, так что остальной код может просто выбрасывать ошибки.
GET /users/99 отвечает 404 с {"error":"No user with id 99"}, а в Console видна строка из обработчика ошибок. Отправьте GET /users/1, и маршрут ответит как обычно; обработчик ошибок срабатывает, только когда что-то пошло не так.
Ошибки, которые Express ловит за вас
Из маршрута до обработчика ошибок можно добраться тремя способами, и в Express 5 все три работают одинаково:
GET /async отвечает 500 с {"error":"Rejected in an async route"}; GET /sync и GET /next попадают в тот же обработчик со своими сообщениями. Теперь уберите next из параметров обработчика ошибок и снова отправьте GET /sync: с тремя параметрами Express считает его обычным middleware, пропускает и отвечает своей стандартной HTML-страницей ошибки.
Async-ошибки: Express 5 и Express 4
Маршрут /async работает, потому что Express 5 ловит отклонённый промис. Express 4 этого не делал: ошибка превращалась в необработанное отклонение, и запрос зависал.
| Express 4 | Express 5 | |
|---|---|---|
throw в синхронном обработчике | Ловится | Ловится |
next(err) | Ловится | Ловится |
Отклонённый промис из async-обработчика | Не ловится, запрос зависает | Ловится |
Ошибка в setTimeout или callback'е события | Не ловится, процесс падает | Не ловится, процесс падает |
В Express 4 оборачивайте каждый async-обработчик или установите пакет express-async-errors:
// Express 4 only
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 db.findUser(req.params.id);
res.json(user);
}));
Последняя строка таблицы одинакова в обеих версиях: Express не видит ошибку, выброшенную в callback'е, который вызывал не он. Превращайте callback API в промисы (у большей части Node есть версия на промисах, например fs/promises) и ждите их через await.
Обработчик 404 и единый формат JSON-ошибки
API удобнее, когда у всех ошибок одинаковая форма. Дайте ошибкам status, превращайте несовпавшие маршруты в ошибку 404, а форматирует их все один обработчик:
GET /nope не совпадает ни с одним маршрутом, поэтому обработчик 404 превращает его в ошибку, и ответ такой: {"error":"Cannot GET /nope","status":404}. Отправьте GET /users/99, и ошибка, выброшенная маршрутом, получит ту же форму. Отправьте POST /users с телом {"name": }: express.json() не может его разобрать и вызывает next() с ошибкой, у которой status равен 400, поэтому ответ будет {"error":"Unexpected token '}', \"{\"name\": }\" is not valid JSON","status":400}.
Не показывайте stack trace
Сообщения ваших собственных HttpError написаны для клиента. Сообщения и стеки всего остального (драйвера базы данных, опечатки, из-за которой вылетел TypeError) могут раскрыть пути к файлам, запросы и версии библиотек. Обработчик выше отправляет Internal server error для любой ошибки 5xx, а настоящую ошибку логирует на сервере. if (res.headersSent) return next(err); покрывает ошибку, случившуюся после того, как часть ответа уже отправлена: тогда единственный безопасный ход это дать Express закрыть соединение.
Стандартный обработчик Express смотрит на NODE_ENV: он включает stack trace в свою HTML-страницу, если NODE_ENV не равен production. Задайте его на продакшен-сервере (NODE_ENV=production node server.js), но не полагайтесь на это: свой обработчик, который никогда не отправляет err.stack, надёжнее.
Частые ошибки
Обработчик ошибок зарегистрирован до маршрутов. Express передаёт ошибки только вперёд, поэтому обработчик выше маршрута никогда не увидит его ошибки. Ставьте его последним, после обработчика 404.
Ошибку поймали и ничего с ней не сделали. try/catch, который только логирует, оставляет запрос висеть. В catch вызовите next(err), выбросьте ошибку снова или отправьте ответ.
Два ответа. Если выбросить ошибку после res.json(), обработчик ошибок попробует отправить ответ ещё раз. Проверяйте res.headersSent, как в примере.
Как работают обычный middleware и next(), читайте в статье Express middleware.
Часто задаваемые вопросы
Можно ли в Express иметь несколько обработчиков ошибок?
Да. Они выполняются по порядку, и каждый либо отправляет ответ, либо вызывает next(err), чтобы передать ошибку дальше. Часто их разделяют так: один логирует, другой отправляет ответ.
Что будет, если обработчик ошибок сам выбросит ошибку?
Express передаст новую ошибку следующему обработчику ошибок, а если его нет, своему встроенному, который отправит страницу 500. Делайте обработчики ошибок простыми, чтобы они сами не могли сломаться.
Какой код статуса ставить в ответе с ошибкой?
4xx, когда клиент прислал что-то не то (400 неверные данные, 401 не выполнен вход, 403 нет доступа, 404 не найдено, 409 конфликт), и 500, когда сломался ваш код. Никогда не отвечайте 200 с ошибкой в теле.
Как вернуть ошибки валидации для нескольких полей?
Отвечайте 400 со списком, например { "errors": [{ "field": "email", "message": "is required" }] }. С zod преобразуйте result.error.issues в такую форму в обработчике ошибок, чтобы все маршруты сообщали о неверных данных одинаково.