Асинхронные ошибки в JavaScript: за пределами `try/catch`

Асинхронность — фундаментальное свойство JavaScript, но обработка ошибок в этом контексте остается источником утечек памяти, лакун в логике и непредсказуемого поведения. Рассмотрим стратегии, превосходящие базовые try/catch, которые защитят ваше приложение от тихих сбоев.

Ограничения стандартных подходов

Типичный async/await с try/catch страдает избыточностью и скрывает контекст:

javascript
async function fetchUserData(userId) {
  try {
    const user = await fetch(`/users/${userId}`);
    const posts = await fetch(`/posts?user=${userId}`);
    return { ...user, posts };
  } catch (error) {
    console.error("Ошибка загрузки данных");
    throw error; // Теряется информация о источникe ошибки
  }
}

Здесь:

  • Неясно, какой запрос провалился.
  • Нет стека вызовов для асинхронных операций.
  • Повторяющийся код при масштабировании.

Стратегии промышленной обработки

Преобразование ошибок

Обогащение ошибки метаданными:

javascript
class NetworkError extends Error {
  constructor(url, statusCode, context) {
    super(`Сбой при обращении к ${url}`);
    this.statusCode = statusCode;
    this.context = context;
  }
}

async function safeFetch(url, options) {
  try {
    const response = await fetch(url, options);
    if (!response.ok) {
      throw new NetworkError(url, response.status, 'HTTP_ERROR');
    }
    return await response.json();
  } catch (error) {
    error.url = url; // Аттач ссылки даже для сетевых сбоев
    throw error;
  }
}

Теперь:

  • Ошибки типизированы.
  • Стек включает кастомный контекст.
  • Логирование получает структурированные данные.

Агрегация ошибок при параллелизме

Promise.all терпит неудачу при первом сбое. Promise.allSettled тайно игнорирует ошибки. Решение — гибридный подход:

javascript
async function fetchAll(resources) {
  const results = await Promise.allSettled(
    resources.map(resource => safeFetch(resource.url, resource.options))
  );

  const errors = results
    .filter(r => r.status === 'rejected')
    .map(r => r.reason);

  if (errors.length > 0) {
    throw new AggregateError(errors, 'Multiple fetching errors');
  }

  return results.map(r => r.value);
}

Паттерн Wrapper

Обертки улучшают читабельность, избегая вложенных try/catch:

javascript
function asyncHandler(fn) {
  return async (...args) => {
    try {
      return await fn(...args);
    } catch (error) {
      addErrorMetadata(error, { function: fn.name });
      throw error;
    }
  };
}

const safeUserData = asyncHandler(async userId => {
  const user = await safeFetch(`/users/${userId}`);
  const posts = await safeFetch(`/posts?user=${userId}`);
  return { ...user, posts };
});

Интеграция с фреймворками

Express: Мидлвар для централизации обработки

javascript
app.use(async (err, req, res, next) => {
  if (err instanceof NetworkError) {
    res.status(502).json({ 
      error: 'Upstream failure', 
      endpoint: err.url 
    });
  } else {
    // Логика для других ошибок
  }
});

React: Элегантное управление состоянием ошибок

javascript
const DataComponent = () => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function load() {
      try {
        setData(await safeUserData(123));
      } catch (err) {
        setError(err.userFacingMessage || 'Service unavailable');
      }
    }
    load();
  }, []);

  return error ? <ErrorBoundary message={error} /> : <DataView data={data} />;
};

Куда отправлять ошибки: полноценная экосистема логгирования

Консоль не подходит для продакшен-аналитики. Сбор ошибок обязателен:

  1. Backend: Отправка в Sentry/DataDog через мидлвар
  2. Frontend: Интеграция Error Boundaries с логгером
javascript
class ReportingBoundary extends React.Component {
  componentDidCatch(error, info) {
    loggingClient.captureException(error, { 
      componentStack: info.componentStack,
      userId: Auth.currentUserId()
    });
  }
}

Ключевые принципы

  • Статический анализ: Используйте TypeScript для защиты от распространенных ошибок.
  • Деградация в приложениях типа PWA: Кеширование стратегий и показателей восстанавливает offline-использование.
  • Трассировка: Коррелируйте ошибки фронтенда с соответствующими логами бэкенда через идентификаторы запросов.

Обработка ошибок является проектной практикой, а не постскриптумом. Итеративное включение этих стратегий приводит к приложениям, которые не просто уведомляют о проблеме, но и предоставляют возможности для быстрого устранения.