Обработка ошибок в Node.js: от хаоса к предсказуемости

Сломанные цепочки промисов, необработанные исключения, утечки конфиденциальной информации в production — все это следствия пренебрежения обработкой ошибок. В экосистеме Node.js, где асинхронность пронизывает каждый слой приложения, отсутствие продуманной стратегии обработки сбоев превращается в рулетку: приложение может работать неделями, но внезапно упасть из-за тривиальной ошибки в третьестепенном модуле.

Рассмотрим типичный антипаттерн в Express-приложении:

javascript
app.get('/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id)
  res.json(user)
})

При отсутствии пользователя с указанным ID метод findById возвращает null, и клиент получает HTTP 200 с пустым телом — семантически некорректно. Более опасный сценарий: необработанное исключение в асинхронном коде приводит к завершению всего процесса Node.js.


Архитектура ошибок: классификация и эскалация

Первичная задача — ввести систему типов ошибок, соответствующую доменной логике. Создайте иерархию классов:

javascript
class AppError extends Error {
  constructor(message, httpStatusCode = 500) {
    super(message)
    this.isOperational = true
    this.httpStatusCode = httpStatusCode
    Error.captureStackTrace(this, this.constructor)
  }
}

class ValidationError extends AppError {
  constructor(details) {
    super('Invalid input data', 400)
    this.details = details
  }
}

class DatabaseError extends AppError {
  constructor(originalError) {
    super('Database operation failed', 503)
    this.originalError = originalError
  }
}

Операционные ошибки (отсутствие прав, некорректный ввод) должны отделяться от программных ошибок (баги в коде). Только операционные можно безопасно возвращать клиенту. В конструкторе AppError флаг isOperational и метод captureStackTrace обеспечивают правильное представление стека.


Централизованный обработчик: последняя линия обороны

Регистрируйте middleware обработки ошибок после всех роутов:

javascript
app.use((err, req, res, next) => {
  if (err instanceof AppError) {
    const response = { 
      error: err.message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
    return res.status(err.httpStatusCode).json(response)
  }

  console.error('FATAL ERROR:', err)
  res.status(500).json({ error: 'Internal server error' })
})

Для критических ошибок (нехватка памяти, необработанные rejection) добавьте обработчики уровня процесса:

javascript
process.on('uncaughtException', (error) => {
  console.error('UNCAUGHT EXCEPTION! Shutting down...', error)
  process.exit(1)
})

process.on('unhandledRejection', (reason) => {
  throw new DatabaseError(reason)
})

Асинхронные ловушки: оборачивание колбэков

Вместо try/catch в каждом обработчике роута используйте обертку:

javascript
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 User.findById(req.params.id)
  if (!user) throw new AppError('User not found', 404)
  res.json(user)
}))

Этот паттерн перехватывает исключения из асинхронных функций и передает их в цепочку middleware. Для сложных сценариев добавьте обработку специфических ошибок БД:

javascript
async function createUser(data) {
  try {
    return await User.create(data)
  } catch (err) {
    if (err.code === 11000) {
      throw new ValidationError({ email: 'Already exists' })
    }
    throw new DatabaseError(err)
  }
}

Логгирование с контекстом

Подключите структурированное логгирование с трейсингом запросов:

javascript
const winston = require('winston')
const { combine, timestamp, json } = winston.format

const logger = winston.createLogger({
  format: combine(timestamp(), json()),
  transports: [new winston.transports.Console()]
})

app.use((req, res, next) => {
  req.requestId = uuid.v4()
  logger.info('Request', {
    requestId: req.requestId,
    method: req.method,
    path: req.path
  })
  next()
})

app.use((err, req, res, next) => {
  logger.error('Error', {
    requestId: req.requestId,
    error: err.message,
    stack: err.stack,
    httpStatusCode: err.httpStatusCode
  })
  next(err)
})

При интеграции с системами мониторинга (Sentry, Datadog) добавляйте контекст запроса: идентификатор пользователя, параметры окружения, версию приложения.


Рефакторинг легаси: инкрементальное улучшение

Для уже существующих приложений начните с:

  1. Добавления middleware обработки uncaught exceptions
  2. Внедрения базового класса AppError в новых модулях
  3. Постепенного перевода колбэков на async/await с оберткой asyncHandler
  4. Ввода структурированного логгирования для критических путей

Избегайте глобальной переделки кода — используйте стратегию обертывания старого кода в контролируемые блоки:

javascript
// legacy-callback.js
module.exports = function legacyHandler(callback) {
  // старый код с колбэками
}

// новый адаптер
const util = require('util')
const legacyHandler = util.promisify(require('./legacy-callback'))

app.get('/legacy-route', asyncHandler(async (req, res) => {
  const data = await legacyHandler()
  // ...
}))

Безопасность и прозрачность

Никогда не раскрывайте внутренние детали ошибок в production:

  • Обрезайте стектрейсы
  • Нормализуйте сообщения об ошибках БД
  • Маскируете чувствительные данные в логах

Для валидации входных данных используйте библиотеки типа Zod с кастомными ошибками:

javascript
const UserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
})

app.post('/register', asyncHandler(async (req, res) => {
  const result = UserSchema.safeParse(req.body)
  if (!result.success) {
    throw new ValidationError(result.error.flatten())
  }
  // ...
}))

Обработка ошибок — не дополнительная функциональность, а основа надежной системы. Инвестиции в эту область уменьшают время восстановления после сбоев, предотвращают потерю данных и упрощают поддержку кода. Не пытайтесь охватить все возможные сценарии сразу — внедряйте практики постепенно, начиная с самых критичных участков, и постоянно анализируйте ошибки из production-окружения через системы мониторинга. Помните: хорошая система ошибок не та, где их полностью избегают, а та, где каждая ошибка становится источником информации для улучшения системы.