> Как обработать результаты двух асинхронных запросов с условиями success, fail и timeout в JavaScript? (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Яндекс

Стек: JavaScript

> Пример ответа

Короткий ответ

Для обработки двух асинхронных запросов с условиями success, fail и timeout используйте Promise.allSettled с AbortController для таймаута или Promise.race с отдельным таймером. Для каждого запроса создайте отдельный промис с обработкой ошибок и таймаутом. Затем объедините результаты через Promise.all с маппингом статусов. Это даёт контроль над каждым сценарием без потери данных при частичном отказе.

Подробное объяснение

Основная сложность - одновременная обработка трёх состояний для каждого запроса: успех, ошибка и превышение времени ожидания. В JavaScript нет встроенного механизма для таймаутов в fetch или XMLHttpRequest, поэтому нужно реализовать его вручную.

Ключевые подходы:

  • AbortController - современный способ отмены запросов и установки таймаута через signal. Позволяет явно прервать запрос и обработать отмену как отдельный кейс.
  • Promise.race - классический метод, где вы запускаете запрос и таймер-промис, который отклоняется через заданное время. Минус - запрос продолжает выполняться в фоне.
  • Promise.allSettled - для сбора результатов обоих запросов независимо от их статуса, что удобно при частичных отказах.

Для каждого запроса создаётся обёртка, которая возвращает промис с тремя возможными исходами: resolved (success), rejected (fail) или rejected с признаком timeout. Затем оба промиса запускаются параллельно, и результаты агрегируются.

Важно различать сетевую ошибку и таймаут - это разные сценарии fail. Таймаут обычно обрабатывается как отдельный тип ошибки, чтобы интерфейс мог показать соответствующее сообщение.

На практике

В реальных проектах (например, React-приложениях) такая логика часто инкапсулируется в кастомный хук или сервисный слой. Типичный сценарий - загрузка данных из двух API, где каждый запрос может упасть по таймауту, и нужно показать частичный результат или повторить только упавший запрос.

Используйте AbortController для каждого запроса отдельно, чтобы таймаут одного не отменял другой. Для обработки результатов удобно создать объект-маппер, который преобразует статусы в единый формат для UI.

Пример кода

JAVASCRIPT
function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw { type: 'timeout', message: 'Request timed out' };
}
throw { type: 'fail', message: error.message };
});
}
async function handleTwoRequests(url1, url2, timeout = 5000) {
const results = await Promise.allSettled([
fetchWithTimeout(url1, timeout),
fetchWithTimeout(url2, timeout)
]);
return results.map(result => {
if (result.status === 'fulfilled') {
return { status: 'success', data: result.value };
}
// result.reason содержит { type, message }
return {
status: result.reason.type === 'timeout' ? 'timeout' : 'fail',
error: result.reason.message
};
});
}
// Использование
handleTwoRequests('/api/data1', '/api/data2', 3000)
.then(results => {
results.forEach((r, i) => {
if (r.status === 'success') console.log(`Request ${i+1}:`, r.data);
else console.warn(`Request ${i+1}: ${r.status} - ${r.error}`);
});
});

Как отвечать на собеседовании

Начните с упоминания AbortController как современного стандарта для таймаутов. Объясните, почему Promise.all не подходит - он падает при первой ошибке, а вам нужна обработка каждого запроса отдельно. Упомяните Promise.allSettled для сбора всех результатов. Покажите понимание разницы между сетевой ошибкой и таймаутом. Если спросят про старые браузеры, добавьте вариант с Promise.race и setTimeout.

Что проверяет интервьюер

  • Понимание асинхронности и управления промисами.
  • Знание современных API (AbortController, Promise.allSettled).
  • Умение проектировать обработку ошибок с разными типами.
  • Способность комбинировать несколько асинхронных потоков.
  • Внимание к деталям: очистка таймеров, различение типов ошибок.

Типичные ошибки

  • Использование Promise.all без учёта, что один отказ убивает весь результат.
  • Забывают очистить таймер после успешного ответа - утечка памяти.
  • Не различают таймаут и ошибку сети, обрабатывая их одинаково.
  • Используют один AbortController для обоих запросов - отмена одного отменяет другой.
  • Пытаются использовать async/await без try/catch для каждого запроса, теряя контроль над отдельными ошибками.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью