Знаете это чувство, когда открываешь контроллер в 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.

Главная фишка — автоматические мапперы. Библиотека смотрит на прилетевшую ошибку и пытается понять, откуда она:

  1. Mongoose: Ловит ValidationError, CastError (кривой ID) и дубликаты (code 11000).

  2. Prisma: Если вы на темной стороне (шучу), библиотека парсит коды P2002, P2003 и другие.

  3. Валидаторы: Поддерживаются Zod и Joi. Ошибки форматируются в одну строчку, типа: email: Invalid email; age: Too small.

  4. JWT: Просроченные или левые токены сразу дают 401 Unauthorized.

Вам не нужно писать if (err instanceof ZodError). Оно просто работает.

🛡 Graceful Shutdown (чтобы не было стыдно перед девопсами)

Когда вы деплоите новую версию, старый под получает сигнал SIGTERM. Если ваше приложение в этот момент просто умрет (как делает process.exit), то все юзеры, у которых прямо сейчас грузился файл или шла транзакция, получат обрыв соединения.

Я встроил в либу утилиту для Graceful Shutdown. Она:

  1. Перестает принимать новые запросы.

  2. Ждет, пока завершатся текущие (через server.close).

  3. Дает вам хук onShutdown, чтобы закрыть коннекты к БД.

  4. Если всё зависло — убивает процесс по таймауту, чтобы не висеть зомби.

Использование элементарное:

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).

Всем чистого кода и зеленых тестов! 🖖