← JavaScript/Promise API: all, race, allSettled#92 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Promise API: all, race, allSettled, any

Какую проблему решают методы Promise

Ты открываешь страницу профиля на GitHub. Браузер должен загрузить: данные пользователя, его репозитории, подписчиков, статистику контрибуций. Это четыре независимых API-запроса.

Наивный подход — ждать каждый по очереди:

const user        = await fetchUser(id)      // 200мс
const repos       = await fetchRepos(id)     // 300мс
const followers   = await fetchFollowers(id) // 150мс
const stats       = await fetchStats(id)     // 250мс
// Итого: 200 + 300 + 150 + 250 = 900мс — пользователь ждёт почти секунду!

Правильный подход — параллельно:

const [user, repos, followers, stats] = await Promise.all([
  fetchUser(id), fetchRepos(id), fetchFollowers(id), fetchStats(id)
])
// Итого: max(200, 300, 150, 250) = 300мс — в 3 раза быстрее!

На основе предыдущих уроков

  • «Промисы» — Promise.all принимает массив промисов
  • «async/await» — await Promise.all внутри async-функции
  • «Деструктуризация» — const [a, b, c] = await Promise.all([...])
  • Promise.all — все или ничего

    Запускает промисы параллельно. Разрешается когда все выполнены. Если хотя бы один отклонён — весь Promise.all отклоняется немедленно:

    const [user, repos, followers] = await Promise.all([
      fetchUser(id),       // 200мс
      fetchRepos(id),      // 300мс
      fetchFollowers(id),  // 150мс
    ])
    // Результат через max(200, 300, 150) = 300мс

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

    Promise.allSettled — ждём всех, ошибки допустимы

    В отличие от all, не прерывается при ошибке. Ждёт все промисы и возвращает массив результатов:

    const results = await Promise.allSettled([
      fetchNews(),      // может упасть
      fetchWeather(),   // может упасть
      fetchStocks(),    // может упасть
    ])
    
    results.forEach((result, i) => {
      if (result.status === 'fulfilled') {
        console.log('Данные:', result.value)
      } else {
        console.warn('Виджет недоступен:', result.reason.message)
      }
    })
    // Один упавший виджет не ломает всю страницу

    Используй для виджетов, аналитики, необязательных данных.

    Promise.race — победитель первый

    Возвращает результат первого завершившегося промиса (fulfilled или rejected):

    // Timeout-паттерн — запрос не должен висеть вечно
    async function withTimeout(promise, ms) {
      const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error(`Timeout ${ms}мс`)), ms)
      )
      return Promise.race([promise, timeout])
    }
    
    const data = await withTimeout(fetchHeavyReport(), 5000)

    Promise.any — первый успешный

    Как race, но игнорирует отклонения. Разрешается при первом fulfilled:

    // Несколько CDN — используем самый быстрый доступный
    const asset = await Promise.any([
      fetch('https://cdn1.example.com/bundle.js'),
      fetch('https://cdn2.example.com/bundle.js'),
      fetch('https://cdn3.example.com/bundle.js'),
    ])
    // Если все три упали — AggregateError

    Когда что использовать

    | Метод | Поведение при ошибке | Когда использовать |

    |---|---|---|

    | Promise.all | Отменяет всё | Все данные обязательны |

    | Promise.allSettled | Продолжает | Ошибки в части допустимы |

    | Promise.race | Первый wins | Таймаут, самый быстрый |

    | Promise.any | Игнорирует ошибки | Первый успешный |

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

    1. Последовательные await для независимых запросов:

    // Медленно — ждём каждый по очереди:
    const a = await fetch('/api/a')
    const b = await fetch('/api/b')   // ждёт пока не завершится /api/a
    
    // Быстро — параллельно:
    const [a, b] = await Promise.all([fetch('/api/a'), fetch('/api/b')])

    2. Не обработали отклонение в Promise.all:

    // Сломано — если один упадёт, вся страница сломается:
    const [user, orders] = await Promise.all([fetchUser(id), fetchOrders(id)])
    
    // Исправлено — оборачиваем в try/catch:
    try {
      const [user, orders] = await Promise.all([fetchUser(id), fetchOrders(id)])
    } catch (e) {
      console.error('Не удалось загрузить:', e.message)
    }

    3. Передают промисы вместо функций:

    // Внимание — промисы запускаются сразу при создании, не при передаче в all:
    const p1 = fetchUser(id)   // запрос уже отправлен!
    const p2 = fetchOrders(id) // этот тоже!
    await Promise.all([p1, p2])  // ждём оба — это нормально

    В реальных проектах

  • React Query: параллельные запросы через useQueries
  • SSR (Next.js): загрузка всех данных страницы параллельно в getServerSideProps
  • Микросервисы: API Gateway ожидает результаты от нескольких сервисов через Promise.all
  • Retry logic: Promise.any для повторных попыток с разными серверами
  • Примеры

    Загрузка дашборда: Promise.all vs sequential, и Promise.allSettled для виджетов

    const delay = ms => new Promise(r => setTimeout(r, ms))
    
    // Симуляция API-эндпоинтов
    async function fetchUser(id) {
      await delay(100)
      return { id, name: 'Алексей Иванов', role: 'admin', plan: 'pro' }
    }
    
    async function fetchStats(id) {
      await delay(200)
      return { orders: 47, revenue: 284500, returns: 3 }
    }
    
    async function fetchRecentOrders(id) {
      await delay(150)
      return [
        { id: 1001, product: 'MacBook Pro', status: 'delivered', amount: 189990 },
        { id: 1002, product: 'AirPods',     status: 'shipped',   amount: 24990 },
      ]
    }
    
    // Необязательные виджеты — могут упасть
    async function fetchNewsWidget() {
      await delay(80)
      return [{ title: 'Новые поступления iPhone 15' }]
    }
    
    async function fetchWeatherWidget() {
      await delay(60)
      throw new Error('Weather API недоступен')  // симулируем ошибку
    }
    
    async function fetchBannerWidget() {
      await delay(120)
      return { text: 'Скидки до 30% на электронику!' }
    }
    
    // Загрузка основных данных — все обязательны
    async function loadDashboard(userId) {
      console.log('Загружаем дашборд...')
      const start = Date.now()
    
      const [user, stats, orders] = await Promise.all([
        fetchUser(userId),
        fetchStats(userId),
        fetchRecentOrders(userId),
      ])
    
      console.log(`Основные данные: ${Date.now() - start}мс (параллельно)`)
      console.log(`Пользователь: ${user.name} [${user.plan}]`)
      console.log(`Выручка: ${stats.revenue.toLocaleString('ru-RU')} ₽`)
      console.log(`Заказов: ${orders.length}`)
    
      // Необязательные виджеты — ошибки не ломают страницу
      const widgets = await Promise.allSettled([
        fetchNewsWidget(),
        fetchWeatherWidget(),  // упадёт!
        fetchBannerWidget(),
      ])
    
      const widgetNames = ['Новости', 'Погода', 'Баннер']
      widgets.forEach((result, i) => {
        if (result.status === 'fulfilled') {
          console.log(`Виджет "${widgetNames[i]}": загружен`)
        } else {
          console.warn(`Виджет "${widgetNames[i]}": недоступен (${result.reason.message})`)
        }
      })
    }
    
    loadDashboard(1)

    Promise API: all, race, allSettled, any

    Какую проблему решают методы Promise

    Ты открываешь страницу профиля на GitHub. Браузер должен загрузить: данные пользователя, его репозитории, подписчиков, статистику контрибуций. Это четыре независимых API-запроса.

    Наивный подход — ждать каждый по очереди:

    const user        = await fetchUser(id)      // 200мс
    const repos       = await fetchRepos(id)     // 300мс
    const followers   = await fetchFollowers(id) // 150мс
    const stats       = await fetchStats(id)     // 250мс
    // Итого: 200 + 300 + 150 + 250 = 900мс — пользователь ждёт почти секунду!

    Правильный подход — параллельно:

    const [user, repos, followers, stats] = await Promise.all([
      fetchUser(id), fetchRepos(id), fetchFollowers(id), fetchStats(id)
    ])
    // Итого: max(200, 300, 150, 250) = 300мс — в 3 раза быстрее!

    На основе предыдущих уроков

  • «Промисы» — Promise.all принимает массив промисов
  • «async/await» — await Promise.all внутри async-функции
  • «Деструктуризация» — const [a, b, c] = await Promise.all([...])
  • Promise.all — все или ничего

    Запускает промисы параллельно. Разрешается когда все выполнены. Если хотя бы один отклонён — весь Promise.all отклоняется немедленно:

    const [user, repos, followers] = await Promise.all([
      fetchUser(id),       // 200мс
      fetchRepos(id),      // 300мс
      fetchFollowers(id),  // 150мс
    ])
    // Результат через max(200, 300, 150) = 300мс

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

    Promise.allSettled — ждём всех, ошибки допустимы

    В отличие от all, не прерывается при ошибке. Ждёт все промисы и возвращает массив результатов:

    const results = await Promise.allSettled([
      fetchNews(),      // может упасть
      fetchWeather(),   // может упасть
      fetchStocks(),    // может упасть
    ])
    
    results.forEach((result, i) => {
      if (result.status === 'fulfilled') {
        console.log('Данные:', result.value)
      } else {
        console.warn('Виджет недоступен:', result.reason.message)
      }
    })
    // Один упавший виджет не ломает всю страницу

    Используй для виджетов, аналитики, необязательных данных.

    Promise.race — победитель первый

    Возвращает результат первого завершившегося промиса (fulfilled или rejected):

    // Timeout-паттерн — запрос не должен висеть вечно
    async function withTimeout(promise, ms) {
      const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error(`Timeout ${ms}мс`)), ms)
      )
      return Promise.race([promise, timeout])
    }
    
    const data = await withTimeout(fetchHeavyReport(), 5000)

    Promise.any — первый успешный

    Как race, но игнорирует отклонения. Разрешается при первом fulfilled:

    // Несколько CDN — используем самый быстрый доступный
    const asset = await Promise.any([
      fetch('https://cdn1.example.com/bundle.js'),
      fetch('https://cdn2.example.com/bundle.js'),
      fetch('https://cdn3.example.com/bundle.js'),
    ])
    // Если все три упали — AggregateError

    Когда что использовать

    | Метод | Поведение при ошибке | Когда использовать |

    |---|---|---|

    | Promise.all | Отменяет всё | Все данные обязательны |

    | Promise.allSettled | Продолжает | Ошибки в части допустимы |

    | Promise.race | Первый wins | Таймаут, самый быстрый |

    | Promise.any | Игнорирует ошибки | Первый успешный |

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

    1. Последовательные await для независимых запросов:

    // Медленно — ждём каждый по очереди:
    const a = await fetch('/api/a')
    const b = await fetch('/api/b')   // ждёт пока не завершится /api/a
    
    // Быстро — параллельно:
    const [a, b] = await Promise.all([fetch('/api/a'), fetch('/api/b')])

    2. Не обработали отклонение в Promise.all:

    // Сломано — если один упадёт, вся страница сломается:
    const [user, orders] = await Promise.all([fetchUser(id), fetchOrders(id)])
    
    // Исправлено — оборачиваем в try/catch:
    try {
      const [user, orders] = await Promise.all([fetchUser(id), fetchOrders(id)])
    } catch (e) {
      console.error('Не удалось загрузить:', e.message)
    }

    3. Передают промисы вместо функций:

    // Внимание — промисы запускаются сразу при создании, не при передаче в all:
    const p1 = fetchUser(id)   // запрос уже отправлен!
    const p2 = fetchOrders(id) // этот тоже!
    await Promise.all([p1, p2])  // ждём оба — это нормально

    В реальных проектах

  • React Query: параллельные запросы через useQueries
  • SSR (Next.js): загрузка всех данных страницы параллельно в getServerSideProps
  • Микросервисы: API Gateway ожидает результаты от нескольких сервисов через Promise.all
  • Retry logic: Promise.any для повторных попыток с разными серверами
  • Примеры

    Загрузка дашборда: Promise.all vs sequential, и Promise.allSettled для виджетов

    const delay = ms => new Promise(r => setTimeout(r, ms))
    
    // Симуляция API-эндпоинтов
    async function fetchUser(id) {
      await delay(100)
      return { id, name: 'Алексей Иванов', role: 'admin', plan: 'pro' }
    }
    
    async function fetchStats(id) {
      await delay(200)
      return { orders: 47, revenue: 284500, returns: 3 }
    }
    
    async function fetchRecentOrders(id) {
      await delay(150)
      return [
        { id: 1001, product: 'MacBook Pro', status: 'delivered', amount: 189990 },
        { id: 1002, product: 'AirPods',     status: 'shipped',   amount: 24990 },
      ]
    }
    
    // Необязательные виджеты — могут упасть
    async function fetchNewsWidget() {
      await delay(80)
      return [{ title: 'Новые поступления iPhone 15' }]
    }
    
    async function fetchWeatherWidget() {
      await delay(60)
      throw new Error('Weather API недоступен')  // симулируем ошибку
    }
    
    async function fetchBannerWidget() {
      await delay(120)
      return { text: 'Скидки до 30% на электронику!' }
    }
    
    // Загрузка основных данных — все обязательны
    async function loadDashboard(userId) {
      console.log('Загружаем дашборд...')
      const start = Date.now()
    
      const [user, stats, orders] = await Promise.all([
        fetchUser(userId),
        fetchStats(userId),
        fetchRecentOrders(userId),
      ])
    
      console.log(`Основные данные: ${Date.now() - start}мс (параллельно)`)
      console.log(`Пользователь: ${user.name} [${user.plan}]`)
      console.log(`Выручка: ${stats.revenue.toLocaleString('ru-RU')} ₽`)
      console.log(`Заказов: ${orders.length}`)
    
      // Необязательные виджеты — ошибки не ломают страницу
      const widgets = await Promise.allSettled([
        fetchNewsWidget(),
        fetchWeatherWidget(),  // упадёт!
        fetchBannerWidget(),
      ])
    
      const widgetNames = ['Новости', 'Погода', 'Баннер']
      widgets.forEach((result, i) => {
        if (result.status === 'fulfilled') {
          console.log(`Виджет "${widgetNames[i]}": загружен`)
        } else {
          console.warn(`Виджет "${widgetNames[i]}": недоступен (${result.reason.message})`)
        }
      })
    }
    
    loadDashboard(1)

    Задание

    Ты разрабатываешь страницу проекта в системе управления задачами (Jira-подобной). При открытии проекта нужно параллельно загрузить: - Детали проекта - Список задач - Список участников А также опционально (через `allSettled`) загрузить виджеты активности и комментарии, которые могут быть недоступны. Покажи разницу во времени: последовательно vs параллельно.

    Подсказка

    Promise.all([fetchProject(id), fetchTasks(id), fetchMembers(id)]). Promise.allSettled([fetchActivity(id), fetchComments(id)]). Время параллельной загрузки = max(100, 200, 150) = 200мс, последовательной = 450мс.

    Загружаем среду выполнения...
    Загружаем AI-помощника...