← JavaScript/Цепочка промисов#98 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Цепочка промисов

В GitHub Actions конвейер сборки выглядит так: скачать зависимости → запустить тесты → собрать бандл → задеплоить. Каждый шаг зависит от предыдущего. В JavaScript это точная аналогия цепочки промисов: каждый .then() получает результат предыдущего и передаёт следующему.

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

Колбэки для последовательных асинхронных операций превращаются в «пирамиду судьбы». Промисы решают это через плоскую цепочку: каждый .then() возвращает новый промис, что позволяет писать последовательный асинхронный код без вложенности.

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

  • Промисы — основы Promise, resolve, reject
  • Колбэки — проблема вложенных колбэков
  • Rest/Spread — работа со spread в примерах
  • Как работает цепочка

    Promise.resolve(1)
      .then(n => n * 2)    // получает 1, возвращает 2
      .then(n => n + 10)   // получает 2, возвращает 12
      .then(n => console.log(n))  // 12

    Каждый .then получает результат предыдущего и передаёт дальше.

    Возврат промиса из .then — автоматическое разворачивание

    Если в .then вернуть промис, следующий .then получит его результат, а не сам промис. Это убирает вложенность:

    fetchUser(1)
      .then(user => fetchOrders(user.id))   // возвращаем промис
      .then(orders => {                      // получаем уже массив заказов
        console.log(orders.length)
      })

    .catch — перехват ошибок

    .catch перехватывает ошибки от всех предыдущих шагов цепочки:

    fetchUser(1)
      .then(user => fetchOrders(user.id))
      .then(orders => filterActive(orders))
      .then(active => console.log(active))
      .catch(err => console.error('Ошибка на любом шаге:', err.message))

    Бросить ошибку в .then — то же что reject:

    .then(user => {
      if (user.banned) throw new Error('Пользователь заблокирован')
      return user
    })

    .finally — всегда выполняется

    .finally запускается независимо от результата — скрыть спиннер, закрыть соединение:

    showSpinner()
    fetchData()
      .then(data => render(data))
      .catch(err => showError(err))
      .finally(() => hideSpinner())  // выполнится в любом случае

    .then(a, b) vs .then(a).catch(b)

    // Разница: если в 'a' бросить ошибку — 'b' в первом случае не поймает её
    promise.then(onFulfilled, onRejected)  // onRejected не ловит ошибки из onFulfilled!
    
    promise
      .then(onFulfilled)
      .catch(onRejected)  // ловит ошибки И от promise, И от onFulfilled

    Предпочитай .then(a).catch(b).

    Восстановление после ошибки (fallback)

    .catch может не завершить цепочку, а вернуть запасное значение:

    fetchUserFromAPI(id)
      .catch(() => fetchUserFromCache(id))  // fallback на кеш при ошибке API
      .then(user => renderUser(user))       // продолжаем в любом случае
      .catch(err => showError(err))         // финальная обработка

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

    Ошибка 1: вложение вместо цепочки

    // Неправильно — callback hell с промисами
    fetchUser(id).then(user => {
      fetchOrders(user.id).then(orders => {  // вложенный .then!
        console.log(orders)
      })
    })
    
    // Правильно — плоская цепочка через return
    fetchUser(id)
      .then(user => fetchOrders(user.id))  // return промис
      .then(orders => console.log(orders))

    Ошибка 2: забыли return в .then

    // Неправильно — следующий .then получит undefined
    fetchUser(id)
      .then(user => {
        fetchOrders(user.id)  // нет return! промис потерян
      })
      .then(orders => console.log(orders))  // orders === undefined
    
    // Правильно
    fetchUser(id)
      .then(user => fetchOrders(user.id))  // стрелочная функция с implicit return
      .then(orders => console.log(orders))

    Ошибка 3: нет .catch в конце

    // Неправильно — ошибка «проглатывается», ничего не видно
    fetchUser(id)
      .then(user => renderUser(user))
    // Если fetchUser упадёт — UnhandledPromiseRejection, тихий баг
    
    // Правильно
    fetchUser(id)
      .then(user => renderUser(user))
      .catch(err => console.error('Ошибка:', err.message))

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

  • GitHub Actions / CI: последовательные шаги сборки
  • Регистрация пользователя: создать аккаунт → отправить email → создать профиль
  • Загрузка файла: получить URL → загрузить → показать превью
  • Обновление данных: получить текущие → изменить → сохранить → обновить UI
  • Примеры

    Конвейер обработки заказа: загрузка → проверка → обогащение данных → форматирование

    function simulateFetch(data, delay = 150, shouldFail = false) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (shouldFail) reject(new Error('Сетевая ошибка'))
          else resolve(data)
        }, delay)
      })
    }
    
    const users = [
      { id: 1, name: 'Иван Петров',  active: true  },
      { id: 2, name: 'Анна Сидорова', active: false },
      { id: 3, name: 'Олег Козлов',  active: true  },
    ]
    const orders = [
      { id: 101, userId: 1, total: 2500, items: 3 },
      { id: 102, userId: 3, total: 890,  items: 1 },
    ]
    
    // Цепочка: загрузить пользователей → оставить активных → обогатить заказами → форматировать
    simulateFetch(users)
      .then(list => list.filter(u => u.active))           // только активные
      .then(active => {
        const ids = active.map(u => u.id)
        const userOrders = orders.filter(o => ids.includes(o.userId))
        return simulateFetch(userOrders)                   // загружаем заказы
      })
      .then(activeOrders => activeOrders.map(order => ({
        ...order,
        summary: `Заказ #${order.id}: ${order.items} товара на ${order.total}₽`
      })))
      .then(result => {
        console.log('Активные заказы:')
        result.forEach(o => console.log(' -', o.summary))
        // - Заказ #101: 3 товара на 2500₽
        // - Заказ #102: 1 товара на 890₽
      })
      .catch(err => console.error('Ошибка в конвейере:', err.message))
      .finally(() => console.log('Загрузка завершена'))
    
    // Пример восстановления после ошибки (fallback)
    const cache = [{ id: 1, name: 'Иван (из кеша)', active: true }]
    
    simulateFetch(null, 100, true)           // API упал
      .catch(() => simulateFetch(cache, 50)) // берём из кеша
      .then(data => console.log('Данные:', data[0].name))
      // 'Данные: Иван (из кеша)'
      .catch(err => console.error('Даже кеш недоступен:', err.message))

    Цепочка промисов

    В GitHub Actions конвейер сборки выглядит так: скачать зависимости → запустить тесты → собрать бандл → задеплоить. Каждый шаг зависит от предыдущего. В JavaScript это точная аналогия цепочки промисов: каждый .then() получает результат предыдущего и передаёт следующему.

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

    Колбэки для последовательных асинхронных операций превращаются в «пирамиду судьбы». Промисы решают это через плоскую цепочку: каждый .then() возвращает новый промис, что позволяет писать последовательный асинхронный код без вложенности.

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

  • Промисы — основы Promise, resolve, reject
  • Колбэки — проблема вложенных колбэков
  • Rest/Spread — работа со spread в примерах
  • Как работает цепочка

    Promise.resolve(1)
      .then(n => n * 2)    // получает 1, возвращает 2
      .then(n => n + 10)   // получает 2, возвращает 12
      .then(n => console.log(n))  // 12

    Каждый .then получает результат предыдущего и передаёт дальше.

    Возврат промиса из .then — автоматическое разворачивание

    Если в .then вернуть промис, следующий .then получит его результат, а не сам промис. Это убирает вложенность:

    fetchUser(1)
      .then(user => fetchOrders(user.id))   // возвращаем промис
      .then(orders => {                      // получаем уже массив заказов
        console.log(orders.length)
      })

    .catch — перехват ошибок

    .catch перехватывает ошибки от всех предыдущих шагов цепочки:

    fetchUser(1)
      .then(user => fetchOrders(user.id))
      .then(orders => filterActive(orders))
      .then(active => console.log(active))
      .catch(err => console.error('Ошибка на любом шаге:', err.message))

    Бросить ошибку в .then — то же что reject:

    .then(user => {
      if (user.banned) throw new Error('Пользователь заблокирован')
      return user
    })

    .finally — всегда выполняется

    .finally запускается независимо от результата — скрыть спиннер, закрыть соединение:

    showSpinner()
    fetchData()
      .then(data => render(data))
      .catch(err => showError(err))
      .finally(() => hideSpinner())  // выполнится в любом случае

    .then(a, b) vs .then(a).catch(b)

    // Разница: если в 'a' бросить ошибку — 'b' в первом случае не поймает её
    promise.then(onFulfilled, onRejected)  // onRejected не ловит ошибки из onFulfilled!
    
    promise
      .then(onFulfilled)
      .catch(onRejected)  // ловит ошибки И от promise, И от onFulfilled

    Предпочитай .then(a).catch(b).

    Восстановление после ошибки (fallback)

    .catch может не завершить цепочку, а вернуть запасное значение:

    fetchUserFromAPI(id)
      .catch(() => fetchUserFromCache(id))  // fallback на кеш при ошибке API
      .then(user => renderUser(user))       // продолжаем в любом случае
      .catch(err => showError(err))         // финальная обработка

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

    Ошибка 1: вложение вместо цепочки

    // Неправильно — callback hell с промисами
    fetchUser(id).then(user => {
      fetchOrders(user.id).then(orders => {  // вложенный .then!
        console.log(orders)
      })
    })
    
    // Правильно — плоская цепочка через return
    fetchUser(id)
      .then(user => fetchOrders(user.id))  // return промис
      .then(orders => console.log(orders))

    Ошибка 2: забыли return в .then

    // Неправильно — следующий .then получит undefined
    fetchUser(id)
      .then(user => {
        fetchOrders(user.id)  // нет return! промис потерян
      })
      .then(orders => console.log(orders))  // orders === undefined
    
    // Правильно
    fetchUser(id)
      .then(user => fetchOrders(user.id))  // стрелочная функция с implicit return
      .then(orders => console.log(orders))

    Ошибка 3: нет .catch в конце

    // Неправильно — ошибка «проглатывается», ничего не видно
    fetchUser(id)
      .then(user => renderUser(user))
    // Если fetchUser упадёт — UnhandledPromiseRejection, тихий баг
    
    // Правильно
    fetchUser(id)
      .then(user => renderUser(user))
      .catch(err => console.error('Ошибка:', err.message))

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

  • GitHub Actions / CI: последовательные шаги сборки
  • Регистрация пользователя: создать аккаунт → отправить email → создать профиль
  • Загрузка файла: получить URL → загрузить → показать превью
  • Обновление данных: получить текущие → изменить → сохранить → обновить UI
  • Примеры

    Конвейер обработки заказа: загрузка → проверка → обогащение данных → форматирование

    function simulateFetch(data, delay = 150, shouldFail = false) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (shouldFail) reject(new Error('Сетевая ошибка'))
          else resolve(data)
        }, delay)
      })
    }
    
    const users = [
      { id: 1, name: 'Иван Петров',  active: true  },
      { id: 2, name: 'Анна Сидорова', active: false },
      { id: 3, name: 'Олег Козлов',  active: true  },
    ]
    const orders = [
      { id: 101, userId: 1, total: 2500, items: 3 },
      { id: 102, userId: 3, total: 890,  items: 1 },
    ]
    
    // Цепочка: загрузить пользователей → оставить активных → обогатить заказами → форматировать
    simulateFetch(users)
      .then(list => list.filter(u => u.active))           // только активные
      .then(active => {
        const ids = active.map(u => u.id)
        const userOrders = orders.filter(o => ids.includes(o.userId))
        return simulateFetch(userOrders)                   // загружаем заказы
      })
      .then(activeOrders => activeOrders.map(order => ({
        ...order,
        summary: `Заказ #${order.id}: ${order.items} товара на ${order.total}₽`
      })))
      .then(result => {
        console.log('Активные заказы:')
        result.forEach(o => console.log(' -', o.summary))
        // - Заказ #101: 3 товара на 2500₽
        // - Заказ #102: 1 товара на 890₽
      })
      .catch(err => console.error('Ошибка в конвейере:', err.message))
      .finally(() => console.log('Загрузка завершена'))
    
    // Пример восстановления после ошибки (fallback)
    const cache = [{ id: 1, name: 'Иван (из кеша)', active: true }]
    
    simulateFetch(null, 100, true)           // API упал
      .catch(() => simulateFetch(cache, 50)) // берём из кеша
      .then(data => console.log('Данные:', data[0].name))
      // 'Данные: Иван (из кеша)'
      .catch(err => console.error('Даже кеш недоступен:', err.message))

    Задание

    Напиши конвейер обработки данных каталога товаров интернет-магазина: загрузи список через simulateFetch, добавь каждому товару поле priceWithTax (цена + 20% НДС, округлённая до 2 знаков), отфильтруй только товары в наличии (inStock: true), отсортируй по цене по возрастанию, выведи результат. Обработай ошибки и добавь .finally.

    Подсказка

    priceWithTax: parseFloat((p.price * 1.2).toFixed(2)). Фильтрация: products.filter(p => p.inStock). Сортировка: [...products].sort((a, b) => a.priceWithTax - b.priceWithTax). Catch: err => console.error("Ошибка:", err.message).

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