Асинхронная обработка ошибок в Node.js: стратегии и подводные камни

Асинхронная природа Node.js, основанная на цикле событий, требует особого подхода к обработке ошибок. Даже опытные разработчики часто оставляют пробелы в обработке исключений, что приводит к падению приложений в продакшене. Решение проблемы требует понимания не только синтаксиса, но и внутренних механизмов выполнения кода.

Где теряются ошибки: базовые сценарии

Рассмотрим классический пример асинхронного чтения файла:

javascript
const fs = require('fs');

function readConfig() {
  fs.readFile('config.json', 'utf8', (err, data) => {
    if (err) console.error('Ошибка чтения');
    return JSON.parse(data);
  });
}

Оставленное без обработки исключение JSON.parse при невалидном JSON приводит к необработанному исключению. В Node.js 16+ это вызывает завершение процесса.

Исправление требует вложенной обработки:

javascript
fs.readFile('config.json', 'utf8', (err, data) => {
  if (err) return console.error('Read error:', err);
  try {
    const config = JSON.parse(data);
  } catch (parseErr) {
    console.error('Parse error:', parseErr);
  }
});

Проблемы с промисами и async/await

Современная практика предполагает использование async/await, но распространена ошибочная структура:

javascript
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

// Вызов без try/catch
fetchData().then(data => ...);

Необработанные ошибки вызовут unhandled rejection. Решение — обязательная обертка:

javascript
async function safeFetch() {
  try {
    const response = await fetch('/api/data');
    return await response.json();
  } catch (err) {
    handleError(err);
    throw err; // Сохраняем стек вызовов
  }
}

Для обработки промисов используйте .catch() с явной передачей ошибки:

javascript
fetchData()
  .then(data => process(data))
  .catch(err => {
    logger.log(err);
    throw new ApplicationError('Data processing failed', { cause: err });
  });

Паттерны для enterprise-приложений

Централизованная обработка

В Express-приложениях создайте middleware для ошибок:

javascript
app.use(async (err, req, res, next) => {
  if (err instanceof DatabaseError) {
    await sendAlertToSRE(err);
    return res.status(503).json({ error: 'Database unavailable' });
  }
  next(err);
});

Обертка асинхронных функций

Декоратор для обработки исключений в маршрутах:

javascript
function asyncHandler(fn) {
  return (req, res, next) => 
    Promise.resolve(fn(req, res, next)).catch(next);
}

app.get('/data', asyncHandler(async (req, res) => {
  const data = await fetchData();
  res.json(data);
}));

Транзакционные операции

Для задач, требующих атомарности:

javascript
async function transactionalUpdate(userId, update) {
  const session = await mongoose.startSession();
  try {
    session.startTransaction();
    const user = await User.findById(userId).session(session);
    await user.updateOne(update).session(session);
    await session.commitTransaction();
  } catch (err) {
    await session.abortTransaction();
    throw new DatabaseTransactionError('Update failed', { cause: err });
  } finally {
    session.endSession();
  }
}

Глубокие проблемы EventEmitter

Ошибки в обработчиках событий часто игнорируются:

javascript
const eventEmitter = new EventEmitter();
emitter.on('data', async (payload) => {
  await processPayload(payload); // Необработанный rejection
});

Решение — обертка в микротаск:

javascript
emitter.on('data', (payload) => {
  process.nextTick(async () => {
    try {
      await processPayload(payload);
    } catch (err) {
      handleError(err);
    }
  });
});

Инструменты мониторинга

Настройте обработчики глобальных событий:

javascript
process.on('unhandledRejection', (reason, promise) => {
  sentry.captureException(reason, { extra: { promise } });
});

process.on('uncaughtException', (err) => {
  sentry.captureException(err);
  process.exit(1); // Обязательное завершение после фатальной ошибки
});

Заключение

Ключевые принципы устойчивой обработки ошибок в Node.js:

  1. Гранулярность: Обрабатывайте исключения на максимально низком уровне, где доступен контекст
  2. Декларативность: Используйте обертки и middlewares для устранения дублирования
  3. Композиция: Сохраняйте оригинальные ошибки через cause (Node.js 16+)
  4. Мониторинг: Интегрируйте глобальные обработчики с системами трейсинга
  5. Консистентность: Документируйте политики обработки ошибок для команды

Планирование обработки исключений на этапе проектирования архитектуры экономит сотни часов отладки. Асинхронные ошибки не могут быть второстепенной задачей — их некорректная обработка превращает приложение в нестабильную систему с непредсказуемым поведением.

text