Асинхронные ошибки в JavaScript: Инженерные стратегии для надежных приложений

Ошибки в асинхронном JavaScript коде – не исключения, а неизбежность. Каждый вызов API, операция с базой данных, или файловая операция несет в себе потенциал сбоя. Как фронтенд, так и бэкенд разработчики сталкиваются с парадоксом: асинхронные операции критически важны, но их ошибки теряют контекст, проваливаются в пустоту, или падают приложение целиком. Рассмотрим системные подходы к обработке асинхронных ошибок за пределами базового try/catch.

Минусы наивного подхода

Главная проблема ошибок в промисах и async/await – потеря стека вызовов. Рассмотрим типичный антипаттерн:

javascript
async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json(); // Ошибка сети игнорируется!
}

Здесь любая сетевая ошибка выдаст необработанное исключение, обрушив процесс Node.js или "ломая" фронтенд. Стандартное решение:

javascript
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  } catch (err) {
    console.error('Fetch failed:', err); // Логирование – это только начало
  }
}

Однако остаются нерешенными вопросы:

  • Как передавать ошибку выше по стеку?
  • Где обрабатывать бизнес-логику восстановления?
  • Как избежать дублирования кода обработки?

Стратегия 1: Комбинированные промисы вместо try/catch

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

javascript
function asyncHandler(promise) {
  return promise
    .then(data => [null, data])
    .catch(err => [err, null]);
}

// Вместо try/catch:
const [userError, userData] = await asyncHandler(fetchUser(userId));
const [orderError, orderData] = await asyncHandler(fetchOrders(userId));

if (userError || orderError) {
  // Единая точка обработки
}

Этот паттерн идеален для параллельных запросов с агрегированием ошибок.

Стратегия 2: Централизованная обработка через EventEmitter

На бэкенде (Node.js) создайте механизм централизованного уведомления об ошибках:

javascript
// errorHandler.js
const EventEmitter = require('events');
class ErrorTracker extends EventEmitter {}
const errorTracker = new ErrorTracker();

errorTracker.on('unhandled', (err, context) => {
  logger.serviceError(err, { context });
  metrics.increment('async_error');
  if (err.critical) process.exit(1);
});

module.exports = errorTracker;

// В коде сервиса
async function refreshCache() {
  try {
    await cache.refresh();
  } catch (err) {
    errorTracker.emit('unhandled', err, { module: 'cache' }); // Перехват контекста
  }
}

Преимущества:

  • Логирование, нотификации и метрики в одном месте
  • Возможность добавить контекст в момент возникновения
  • Разделение бизнес-логики и инфраструктуры

Стратегия 3: Кастомные ошибки с трейсами и контекстом

Генерируйте ошибки со структурными метаданными для автоматизированного анализа:

javascript
class DatabaseError extends Error {
  constructor(message, { query, params, code }) {
    super(message);
    this.name = 'DatabaseError';
    this.details = { query, params, code };
    Error.captureStackTrace(this, DatabaseError);
  }
}

// В DAL-слое
async function queryDB(sql, params) {
  try {
    return await pool.query(sql, params);
  } catch (dbErr) {
    throw new DatabaseError('DB query failed', { 
      query: sql, 
      params, 
      code: dbErr.code 
    });
  }
}

На фронтенде эти ошибки можно сериализовать для devtools, на бэкенде – агрегировать в PRM-системах.

Стратегия 4: Глобальные обработчики как страховка

Регистрируйте крайнюю линию защиты:

javascript
// Node.js
process.on('unhandledRejection', (reason) => {
  errorTracker.emit('unhandled', reason);
});

// Браузер
window.addEventListener('unhandledrejection', (event) => {
  telemetry.send('UNHANDLED_REJECTION', event.reason);
});

Важно: это должен быть не основной механизм, а система оповещения о пропущенных кейсах.

Выводы: Правила для production-кода

  1. Контролируйте цепные реакции
    Всегда выполняйте .catch() для промисов без await
    Используйте Promise.allSettled() для массовых операций

  2. Добавляйте семантику
    Преобразовывайте низкоуровневые ошибки в доменные
    Обогащайте контекстом (id задачи, юзера, параметры запроса)

  3. Разделяйте управление ошибками
    Обработчики на уровне приложения – для протоколирования
    ПО промежуточного слоя – для HTTP-ответов
    Доменные функции – для синтаксических проверок

  4. Автоматизируйте реакцию
    Интегрируйте с системами мониторинга (Sentry, DataDog)
    Настройте алерты по коду ошибки и частоте возникновения

Обработка асинхронных ошибок – проектирование failure paths с таким же вниманием, как и happy paths. Не используйте try/catch как костыль для игнорирования проблем. Формализованная стратегия для ошибок обеспечивает предсказуемое поведение системы при частичных сбоях и ускоряет локализацию инцидентов.