Знаете это чувство, когда открываешь контроллер в Express проекте, чтобы поправить одну строчку логики, и видишь ЭТО? Бесконечная вложенность, проверки на существование полей, ручной парсинг ошибок от базы данных и, конечно же, его величество try-catch, который занимает 80% файла.
Я тоже через это проходил. В каждом новом микросервисе я копипастил одни и те же функции обработки ошибок. В одном проекте я ловил ошибки Mongoose через err.name === 'ValidationError', в другом — через instanceof. Где-то мы отдавали { error: "message" }, где-то { status: "fail", msg: "..." }.
В какой-то момент мне это надоело. Мне захотелось инструмент, который я могу просто подключить одной строкой, и он сам поймет, что "E11000" от Mongo — это 409 Conflict, а ошибка Zod — это 400 Bad Request. При этом я не хотел тянуть в проект тяжелые зависимости.
Так родилась библиотека ds-express-errors. Сегодня я расскажу, зачем я ее написал и почему она может сэкономить вам кучу нервов.
В чем, собственно, боль?
Давайте посмотрим на типичный код джуна (да и не только), который пишет регистрацию юзера:
// Типичный "страшный сон" app.post('/register', async (req, res) => { try { const { email, password } = req.body; // 1. Ручная валидация if (!email) return res.status(400).json({ error: 'Email нужен' }); // 2. Сама логика const user = await User.create({ email, password }); res.status(201).json(user); } catch (err) { // 3. Ад обработки ошибок console.error(err); // Ну хоть в консоль плюнем if (err.code === 11000) { return res.status(409).json({ error: 'Такой email уже есть' }); } if (err.name === 'ValidationError') { // Тут обычно пишут map, чтобы достать сообщения return res.status(400).json({ error: 'Ошибка валидации...' }); } res.status(500).json({ error: 'Всё упало' }); } });
Это больно читать. Это больно поддерживать. И самое главное — это нужно писать в каждом контроллере.
?) Как я это решил (aka "Магия")
Я хотел, чтобы код выглядел так: «Пробуем сделать действие. Если не вышло — кидаем понятную ошибку. Если база ругается — пусть библиотека сама разбирается».
Вот тот же самый код с использованием моей либы:
const { asyncHandler, Errors } = require('ds-express-errors'); // Оборачиваем в asyncHandler, чтобы забыть про try-catch app.post('/register', asyncHandler(async (req, res) => { const { email, password } = req.body; // Просто делаем запрос. Если email занят, либа сама перехватит ошибку Mongoose, // поймет, что это дубликат, и отдаст 400-й код с понятным сообщением. const user = await User.create({ email, password }); // Если нужно кинуть свою ошибку: if (!user) throw Errors.BadRequest('Не удалось создать'); res.status(201).json(user); }));
Всё. Никакого шума. Только бизнес-логика.
Что под капотом?
Я написал централизованный errorHandler middleware, который вешается в конце цепочки Express.
Главная фишка — автоматические мапперы. Библиотека смотрит на прилетевшую ошибку и пытается понять, откуда она:
Mongoose: Ловит
ValidationError,CastError(кривой ID) и дубликаты (code 11000).Prisma: Если вы на темной стороне (шучу), библиотека парсит коды P2002, P2003 и другие.
Валидаторы: Поддерживаются Zod и Joi. Ошибки форматируются в одну строчку, типа:
email: Invalid email; age: Too small.JWT: Просроченные или левые токены сразу дают 401 Unauthorized.
Вам не нужно писать if (err instanceof ZodError). Оно просто работает.
🛡 Graceful Shutdown (чтобы не было стыдно перед девопсами)
Когда вы деплоите новую версию, старый под получает сигнал SIGTERM. Если ваше приложение в этот момент просто умрет (как делает process.exit), то все юзеры, у которых прямо сейчас грузился файл или шла транзакция, получат обрыв соединения.
Я встроил в либу утилиту для Graceful Shutdown. Она:
Перестает принимать новые запросы.
Ждет, пока завершатся текущие (через
server.close).Дает вам хук
onShutdown, чтобы закрыть коннекты к БД.Если всё зависло — убивает процесс по таймауту, чтобы не висеть зомби.
Использование элементарное:
const { initGlobalHandlers, gracefulHttpClose } = require('ds-express-errors'); const server = app.listen(3000); initGlobalHandlers({ closeServer: gracefulHttpClose(server), onShutdown: async () => { await mongoose.disconnect(); // Чистим за собой console.log('БД закрыта, уходим в закат'); } });
Теперь ваши графики ошибок 5xx при деплое будут чистыми.
📦 Zero Dependencies
Это мой пунктик. Я ненавижу, когда ставишь маленькую либу, а она тянет за собой половину npm.
В package.json у ds-express-errors ровно 0 (ноль) зависимостей в dependencies. Она весит копейки, быстро ставится и не создает дыр в безопасности через third-party пакеты.
При этом она написана на JS, но внутри лежат .d.ts файлы, так что автокомплит в VS Code и поддержка TypeScript работают из коробки.
Как попробовать?
Библиотека доступна в npm.
npm install ds-express-errors
В коде (обычно в app.js или index.js):
const { errorHandler } = require('ds-express-errors'); // ... ваши роуты ... // Важно: подключаем в самом конце, ПОСЛЕ всех роутов app.use(errorHandler);
Итоги
Я писал эту библиотеку в первую очередь для себя, чтобы перестать копипастить код между проектами. Но сейчас она выросла в полноценный инструмент, который закрывает много потребностей при работе с ошибками в Node.js API.
Если вам тоже надоело писать бойлерплейт — попробуйте.
🔗 Доки и сайт: ds-express-errors.dev 🐙 GitHub: ds-express-errors
Буду рад любым звездам, оценкам и issues. Пишите в комменты, если есть идеи, что еще добавить (сейчас думаю над поддержкой TypeORM).
Всем чистого кода и зеленых тестов! 🖖
