Инженерная практика

Ошибки Node.js API: контракт ответа вместо случайного catch

Разделяем ошибки входных данных, отсутствие ресурса и внутренние сбои. Пишем простой контракт API, не раскрываем stack trace и проверяем границы.

Клиент получает 500, а в логе лежит строка «Something went wrong». Или наоборот: API возвращает клиенту сообщение драйвера базы с SQL и именем таблицы. Обе ситуации показывают отсутствие границы между публичным контрактом и внутренней диагностикой. Хорошая обработка ошибки помогает продолжить сценарий и выяснить причину сбоя, не смешивая эти задачи.

Назовите ожидаемые исходы операции

Для GET пользователя по id нужны хотя бы три исхода: входной параметр некорректен, пользователь не найден, данные получены. Отдельно возможен неожиданный сбой хранилища. Отсутствующий ресурс не обязан становиться исключением внутри доменной функции: иногда null - понятный результат. Но транспортный слой должен последовательно преобразовать его в ответ API.

HTTP-статус отражает класс исхода, стабильный код внутри JSON помогает клиенту принять решение. Текст предназначен человеку и со временем может измениться или перевестись. Поэтому фронтенд не должен сравнивать error.message === «Пользователь не найден». Такой код сломается после редакционной правки, хотя бизнес-смысл останется прежним.

Небольшой контракт без привязки к фреймворку

TypeScript// Ожидаемая ошибка хранит только публичные сведения export class PublicError extends Error { constructor( public readonly status: number, public readonly code: string, message: string, ) { super(message) } } export function errorResponse(error: unknown): Response { const known = error instanceof PublicError return Response.json({ error: { code: known ? error.code : 'INTERNAL_ERROR', message: known ? error.message : 'Не удалось выполнить запрос', }, }, { status: known ? error.status : 500 }) }

PublicError следует создавать только из заранее определённых безопасных сообщений. Не оборачивайте в неё исходное сообщение SQL-драйвера или провайдера. В этом учебном контракте статус хранится числом; в проекте сузьте его до согласованного набора клиентских ошибок. Не позволяйте пользовательскому вводу определять статус или публичный код.

TSX// Транспортная граница для GET пользователя type User = { id: number; name: string } type Repository = { find: (id: number) => Promise<User | null> } export async function getUserResponse( rawId: string, repository: Repository, report: (error: unknown) => void, ): Promise<Response> { try { if (!/^[1-9][0-9]*$/.test(rawId)) { throw new PublicError(400, 'INVALID_ID', 'Нужен целочисленный id') } const id = Number(rawId) if (!Number.isSafeInteger(id)) { throw new PublicError(400, 'INVALID_ID', 'id вне допустимого диапазона') } const user = await repository.find(id) if (!user) throw new PublicError(404, 'USER_NOT_FOUND', 'Пользователь не найден') return Response.json(user) } catch (error) { if (!(error instanceof PublicError)) report(error) return errorResponse(error) } }

Функция report - зависимость для диагностики. В настоящем приложении её сбой не должен сорвать отправку ответа: используйте устойчивый адаптер логирования. Пример не реализует авторизацию и ограничение частоты запросов. До чтения пользователя транспортный слой должен проверить право на эту операцию. Правильная обработка ошибок не заменяет контроль доступа.

Сохраняйте внутреннюю причину, а не раскрывайте её

При преобразовании низкоуровневой ошибки в ошибку приложения можно сохранять cause. Это помогает при анализе связи между отказом хранилища и операцией. Но cause и stack должны оставаться в защищённом контуре диагностики. Для связи ответа с логом полезен идентификатор запроса, который создаёт или проверяет сервер; не записывайте без разбора входные заголовки и тело.

Если каждый слой логирует одну и ту же ошибку, один сбой выглядит как множество независимых инцидентов. Выберите место регистрации и договоритесь, где добавляется контекст. Не стирайте cause строкой String(error) слишком рано. При этом сбор целого объекта запроса ради удобства может сохранить токены и персональные данные: объём диагностики должен быть осознанным.

Что проверить автоматически

  • INVALID_ID: отрицательные, дробные, пустые и слишком большие значения дают 400 и не вызывают репозиторий.
  • USER_NOT_FOUND: корректный id и null от репозитория дают 404 с согласованным кодом.
  • Успех: ответ 200 содержит ожидаемые данные, а не обёрнутую ошибку.
  • Сбой репозитория: ответ 500 не содержит исходный message и stack; диагностика получает причину ровно на выбранной границе.
  • Фронтенд: сетевой отказ и ответ с error обрабатываются отдельно; повтор запроса не очищает пользовательский ввод.

Async, события и перезапуск

catch вокруг await ловит rejected Promise из этой цепочки. Он не перехватывает произвольное событие error у EventEmitter и не покрывает callback, который запустится позже вне цепочки. Проверяйте контракт конкретного API. После неожиданного состояния процесса не пытайтесь сделать вид, что всё исправлено одним глобальным обработчиком: стратегия завершения и восстановления процесса требует отдельного проектирования.

Попробуйте использовать пример с тестовым репозиторием, затем подключить его к проверке unknown на границе клиента. API-контракт входит в backend-программу вместе с данными, тестами и инфраструктурой.

Документация и ограничения

Механизмы ошибок и cause: Node.js 22 - Errors. Значения статусов: MDN HTTP Status. Пример демонстрирует транспортный контракт, а не готовый защищённый endpoint.

Вопросы по теме

Можно ли отдавать message любого Error клиенту?
Нет. Неожиданная ошибка может содержать SQL, пути, адреса и другие внутренние сведения. Показывайте заранее определённый безопасный текст, а причину сохраняйте в защищённой диагностике.
Почему catch вокруг await не ловит все ошибки Node.js?
Node.js использует разные механизмы ошибок: throw, rejected Promise, callbacks и события error. Обработчик должен соответствовать механизму конкретного API.
Стоит ли автоматически повторять любой неудачный запрос?
Нет. Учитывайте тип ошибки, идемпотентность операции и лимит попыток. Повтор записи без подходящего контракта может создать дубликат.

Читайте также

Все статьи

Обсудим вашу подготовку?

Напишите, что уже умеете и с чем возникли трудности. Если есть проект, пришлите ссылку на него.

Отвечаю лично, поэтому прошу рассказать о ситуации подробно. Все поля обязательны, кроме отмеченных. После отправки покажу ссылку на мой Telegram: напишите туда, и начнём разговор.

Что не получается, что уже пробовали. От 100 знаков, сейчас 0.
Где работали или учились, сколько лет, какой стек, сколько откликов и собеседований. От 100 знаков, сейчас 0.
PDF или DOCX, до 5 МБ. Файл видит только администратор.
GitHub, hh, LinkedIn, сайт. Можно несколько, через пробел или с новой строки.