Обработка асинхронных ошибок в JavaScript: избавляемся от типичных проблем

Каждый разработчик, работающий с асинхронным кодом в JavaScript, рано или поздно сталкивается с ошибками, которые «проглатываются» без логов, неожиданными падениями приложения или неконтролируемыми состояниями в цепочках промисов. Эти проблемы особенно коварны в продакшен-среде, где их сложно воспроизвести и отладить. Разберемся, как построить надежную систему обработки ошибок — от базовых паттернов до архитектурных решений.

Почему ошибки «исчезают»?

Рассмотрим типичный сценарий с fetch:

javascript
fetch('/api/data')
  .then(response => response.json())
  .then(data => updateUI(data));

Если сервер вернет 500 ошибку или JSON окажется некорректным, исключение может остаться неперехваченным. В браузере это приведет к ошибке в консоли, но в Node.js процесс вообще завершится с кодом 1. Решение кажется очевидным — добавить .catch(), но на практике разработчики часто забывают обрабатывать ошибки в цепочках промисов или используют async/await без try/catch.

Ловушка Partial Error Handling

Даже при наличии обработчиков часто встречается неполное покрытие:

javascript
async function fetchData() {
  try {
    const res = await fetch('/api');
    const data = await res.json();
    return processData(data);
  } catch (error) {
    console.error('Fetch failed:', error);
  }
}

Здесь не обрабатываются:

  • Ошибки в processData()
  • Случаи, когда res.ok === false
  • Прерывание запроса через AbortController

Стратегии полного покрытия

1. Обертка для HTTP-ответов

Создайте утилиту для проверки статуса:

javascript
async function safeFetch(url, options) {
  const res = await fetch(url, options);
  if (!res.ok) {
    const error = new Error(`HTTP ${res.status}`);
    error.response = res;
    throw error;
  }
  return res;
}

Теперь любой ответ с кодом ≥400 будет вызывать исключение.

2. Комбинирование Promise.catch и async/await

Для сложных цепочек используйте гибридный подход:

javascript
async function loadData() {
  const rawData = await fetch('/api')
    .then(handleInterceptors)
    .catch(error => {
      throw new DataLoadError(error.message, { cause: error });
    });
  
  try {
    return validateData(rawData);
  } catch (validationError) {
    throw new DataLoadError('Invalid format', { 
      cause: validationError 
    });
  }
}

Кастомные классы ошибок сохраняют контекст:

javascript
class DataLoadError extends Error {
  constructor(message, { cause }) {
    super(message);
    this.name = 'DataLoadError';
    this.cause = cause;
  }
}

3. Глобальные обработчики

Для критичных приложений добавьте страхующие механизмы:

javascript
// Браузер
window.addEventListener('unhandledrejection', e => {
  logToService(e.reason);
  e.preventDefault();
});

// Node.js
process.on('unhandledRejection', (reason, promise) => {
  logToService(reason);
  process.exit(1);
});

Но не заменяйте ими локальные обработчики — это крайняя мера.

Интеграция с мониторингом

Логирование ошибок в консоль бесполезно для продакшена. Интегрируйте:

  • Sentry/Bugsnag для фронтенда
  • OpenTelemetry + Grafana Loki для бэкенда
  • Трейсинг ошибок через X-Request-Id

Пример для Node.js с Express:

javascript
app.use(async (err, req, res, next) => {
  const errorId = uuidv4();
  await sendToSentry({
    id: errorId,
    error: err,
    context: {
      route: req.path,
      params: req.params,
      user: req.user?.id
    }
  });
  
  res.status(500).json({ 
    error: 'Internal Error',
    reference: errorId 
  });
});

Паттерны для сложных систем

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

Для последовательности асинхронных вызовов используйте компенсирующие действия:

javascript
async function placeOrder(userId, items) {
  const steps = [];
  try {
    const payment = await createPayment(userId, items);
    steps.push(() => refundPayment(payment.id));
    
    const inventoryReservation = await reserveInventory(items);
    steps.push(() => releaseInventory(inventoryReservation.id));
    
    await notifyShippingService(items);
    return { success: true };
  } catch (error) {
    for (const compensate of steps.reverse()) {
      await compensate();
    }
    throw error;
  }
}

Отказоустойчивые цепочки

Для ненадежных сторонних API реализуйте retry с экспоненциальным откатом:

javascript
async function resilientFetch(url, options, retries = 3) {
  try {
    return await fetch(url, options);
  } catch (error) {
    if (retries <= 0) throw error;
    const delay = 100 * Math.pow(2, 4 - retries);
    await new Promise(r => setTimeout(r, delay));
    return resilientFetch(url, options, retries - 1);
  }
}

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

  • Единой стратегии для клиента и сервера
  • Детального контекста в ошибках
  • Интеграции с мониторингом
  • Регламента обработки в стиле кода

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

text