Мастерство обработки ошибок с async/await: За рамками `try/catch`

Асинхронный код на JavaScript – фундамент современных веб-приложений. Однако элегантность async/await часто оборачивается наивной обёрткой в try/catch, прячущей критические проблемы. Рассмотрим практические стратегии обработки сбоев там, где стандартные подходы бьют мимо цели.

Почему try/catch недостаточно

Попробуем получить данные через API:

javascript
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Ошибка загрузки:', error);
  }
}

Что не так с этим подходом?

  1. HTTP-ошибки не ловятся: fetch не считает ответы 4xx/5xx ошибкой (reject), они проходят в успешный блок.
  2. Потеря контекста: Один catch на все возможные ошибки усложняет диагностику.
  3. Молчаливое проглатывание ошибки: console.error бесполезен в production.

Обрабатываем реальные ошибки HTTP

Доработаем функцию:

javascript
async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  
  // Обрабатываем нестатусы HTTP программно
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }

  // Ошибка десериализации - не становится process exception
  return await response.json(); 
}

Это лучше, но если response.json() упадёт из-за битого JSON, ошибка выскочит наружу без указания источника данных – диагностика усложняется.

Работа с параллельными запросами

Используем Promise.all:

javascript
async function fetchDashboardData() {
  const [posts, comments] = await Promise.all([
    fetchPosts(),
    fetchComments()
  ]);
  // ...
}

Бомба замедленного действия: любой из запросов рухнет – падает вся операция. Решение:

javascript
const results = await Promise.allSettled([
  fetchPosts(),
  fetchComments()
]);

const postsResult = results[0];
const commentsResult = results[1];

// Детальная обработка статуса операции:
if (postsResult.status === 'rejected') {
  logError('Posts:', postsResult.reason, { severity: 'HIGH' });
  return cachedPosts();
}

Интеграция с обработчиками глобальных событий

Дополните локальную обработку глобальным мониторингом:

javascript
// Браузер:
window.addEventListener('unhandledrejection', event => {
  sendToLoggingService(event.reason);
});

// Node.js:
process.on('unhandledRejection', (reason, promise) => {
  logClusterError(reason);
});

Но не глушите глобальные обработчики! Они – последний рубеж для критичных падений.

Абстракция обработчика ошибок

Убираем дублирование кода при помощи высокоуровневой обёртки:

javascript
const errorWrapper = handleErrors(async (fn, ...args) => {
  await fn(...args);
});

const safeFetch = errorWrapper(async (url) => {
  const res = await fetch(url);
  if (!res.ok) throw new HttpError(res.status);
  return res.json();
});

// Используем с контекстом
const user = await safeFetch('/api/users/1', { 
  meta: { component: 'UserView' } 
});

Расширяем handleErrors для интеграции аналитики и контекста:

javascript
export const handleErrors = (fn) => async (...args) => {
  try {
    return await fn(...args);
  } catch (error) {
    // Прикрепляем мета-данные к ошибке
    error.meta = args.meta || {}; 
    errorLogger.capture(error);
    
    // Возвращаем fallback-результат или сбрасываем отображаемую ошибку
    return getFallbackDataFor(error);
  }
};

Ошибки времени ожидания

Простой таймаут для медленных соединений:

javascript
async function fetchWithTimeout(resource, timeout = 3000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);

  const response = await fetch(resource, {
    signal: controller.signal
  });
  clearTimeout(id);

  // Оставшаяся логика ...
}

Обратите внимание: отмена через AbortController передаёт в Promise rejection с AbortError.

Интеграция в архитектурные паттерны

Подключение middleware в Express/Koa:

javascript
// Express middleware ошибок
app.use(async (err, req, res, next) => {
  const userMessage = customMessageMap[err.code] || 'Ошибка системы';
  
  if (err.isOperational) {
    res.status(err.status).json({ error: userMessage });
  } else {
    res.status(500).end();
    await reportCriticalFailure(err);
  }
});

В React-компоненте с использованием Error Boundaries:

javascript
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info.componentStack);
  }
  
  render() {
    if (this.state.hasError) return <FallbackUI />; 
    return this.props.children;
  }
}

Эволюция ошибок в production

Используйете принципы:

  1. Классификация: Разделение ошибок на операционные (ожидаемые) и программные (баги).
  2. Обогащение контекстом: Автоматическое добавление user ID, сессии, метаданных запроса.
  3. Ступенчатая отчетность: Разные ошибки – разные уровни алертинга.

Пример работы с операционными ошибками:

javascript
class DatabaseError extends Error {
  constructor(details) {
    super('Ошибка базы данных');
    this.isOperational = true;
    this.code = 'DB_CONNECT_FAIL';
    this.details = details; // Технические детали для логов
  }
}

// Использование
throw new DatabaseError({
  query: params.query, 
  host: config.dbHost
});

Шепчущий код ошибки

Глубоко проработанная обработка ошибок меняет само использование асинхронных вызовов:

javascript
const { error, data } = await safeLoadData('/items');
if (error) {
  showToast(`Не удалось: ${error.userMessage}`);
  return handlePartialFailure(data);
}

processItems(data);

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

  • Конвейерная обработка ошибок и данных
  • Чистая семантика для критического и ожидаемого сбоя
  • Готовые данные для деградированного режима при гибридном повдении

Итоговый план мониторинга

Рабочий цикл обработки ошибок в продакшене включает:

  1. Дебаг-проваливание: точное определение места сбоя с помощью stack trace.
  2. Агрегация: объединение одинаковых ошибок в инциденты.
  3. Триггеры: автоматические оповещения при превышении порогов.
  4. Сброс состояния: автоматические откаты при обнаружении непреодолимого сбоя.

Ошибки – это не дыры в системе, а каналы обратной связи от реальности. Асинхронная обработка сбоев требует не обёртки блоком try/catch, а проектирования отказоустойчивых интерфейсов взаимодействия с ненадёжным окружением. В этом ключ к стабильности приложения.