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

Промисы

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

Открываешь GitHub — страница начинает загружать сразу всё: твой профиль, репозитории, уведомления, pull requests. JavaScript однопоточный, но браузер умеет делать сетевые запросы «в фоне». Вопрос: как получить результат, когда он придёт?

Раньше использовали колбэки — и получали «ад колбэков»:

// Callback hell — каждый уровень требует ещё одного колбэка
getUser(id, function(user) {
  getRepos(user.login, function(repos) {
    getCommits(repos[0].id, function(commits) {
      render(user, repos, commits)  // вложенность растёт до бесконечности
    }, onError)
  }, onError)
}, onError)

Промис (Promise) — объект-обёртка для асинхронной операции. Он позволяет строить цепочки вместо пирамид вложенности.

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

  • «Функции» — колбэки, передача функций как аргументов
  • «try/catch» — обработка ошибок (в промисах — через .catch)
  • «Колбэки» — откуда растёт проблема, которую решают промисы
  • Три состояния промиса

  • pending — ожидание: операция ещё не завершена
  • fulfilled — успех: resolve(value) был вызван
  • rejected — ошибка: reject(error) был вызван
  • Состояние меняется только один раз и необратимо.

    Создание промиса

    const loadUser = new Promise((resolve, reject) => {
      // Асинхронная операция
      setTimeout(() => {
        const user = { id: 1, name: 'Иван' }
        if (user) {
          resolve(user)                   // передаём результат
        } else {
          reject(new Error('Не найден'))  // передаём ошибку
        }
      }, 300)
    })

    .then, .catch, .finally

    loadUser
      .then(user => {
        console.log('Пользователь:', user.name)
        return user.id  // возвращаем значение для следующего .then
      })
      .then(id => console.log('ID:', id))     // получаем то, что вернул предыдущий .then
      .catch(e => console.error('Ошибка:', e.message))   // ловим любую ошибку из цепочки
      .finally(() => console.log('Загрузка завершена'))  // всегда

    Ключевой момент: каждый .then() возвращает новый промис. Это позволяет строить цепочки.

    Цепочки промисов — вместо вложенности

    // Вместо вложенных колбэков — плоская цепочка
    getUser(1)
      .then(user => getRepos(user.login))       // возвращаем промис
      .then(repos => getCommits(repos[0].id))   // получаем его результат
      .then(commits => render(commits))
      .catch(e => showError(e.message))

    Promise.resolve и Promise.reject

    // Уже выполненный промис — удобно в тестах и дефолтных значениях
    const p1 = Promise.resolve({ id: 1, name: 'Тест' })
    p1.then(user => console.log(user.name))  // 'Тест'
    
    // Уже отклонённый промис
    const p2 = Promise.reject(new Error('Сервер недоступен'))
    p2.catch(e => console.log(e.message))   // 'Сервер недоступен'

    Promise.all — параллельные запросы

    Запускает несколько промисов одновременно и ждёт все:

    // Вместо последовательных 300мс + 200мс + 150мс = 650мс...
    // Параллельно: max(300, 200, 150) = 300мс
    const [user, repos, notifications] = await Promise.all([
      fetchUser(id),          // 300мс
      fetchRepos(id),         // 200мс
      fetchNotifications(id), // 150мс
    ])
    // Всё пришло через 300мс!

    Если хотя бы один промис отклоняется — весь Promise.all отклоняется немедленно.

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

    1. Не возвращают промис из .then():

    // Сломано — вложенный промис, а не цепочка:
    getUser(1)
      .then(user => {
        getRepos(user.id)  // нет return — следующий .then не получит repos!
      })
      .then(repos => console.log(repos))  // repos === undefined
    
    // Исправлено:
    getUser(1)
      .then(user => getRepos(user.id))  // return обязателен
      .then(repos => console.log(repos.length))

    2. Забывают .catch():

    // Сломано — ошибка "улетит" без обработки (UnhandledPromiseRejection):
    getUser(1)
      .then(user => render(user))
      // нет .catch — если getUser упадёт, узнаем только из логов Node.js
    
    // Исправлено:
    getUser(1)
      .then(user => render(user))
      .catch(e => showErrorMessage(e.message))

    3. Создают Promise там, где он уже есть:

    // Сломано — лишняя обёртка:
    function getUser(id) {
      return new Promise((resolve, reject) => {
        fetch(`/api/users/${id}`)
          .then(r => r.json())
          .then(resolve)
          .catch(reject)
      })
    }
    
    // Исправлено — fetch уже возвращает промис:
    function getUser(id) {
      return fetch(`/api/users/${id}`).then(r => r.json())
    }

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

  • Fetch API — возвращает промис, строишь цепочку
  • React: useEffect с промисами для загрузки данных
  • Node.js: чтение файлов, работа с БД — всё возвращает промисы
  • Promise.all — параллельная загрузка данных для дашборда
  • Примеры

    API GitHub: цепочка промисов и параллельная загрузка данных

    // Симуляция GitHub API
    function fetchUser(login) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (!login) return reject(new Error('Login обязателен'))
          resolve({ login, name: 'Иван Петров', publicRepos: 42, followers: 318 })
        }, 100)
      })
    }
    
    function fetchRepos(login) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve([
            { name: 'awesome-project', stars: 124, lang: 'JavaScript' },
            { name: 'my-blog', stars: 18, lang: 'TypeScript' },
            { name: 'utils-lib', stars: 67, lang: 'JavaScript' },
          ])
        }, 150)
      })
    }
    
    function fetchFollowers(login) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve([
            { login: 'anna', name: 'Анна Смирнова' },
            { login: 'boris', name: 'Борис Иванов' },
          ])
        }, 80)
      })
    }
    
    // Цепочка: получаем пользователя, потом его репозитории
    console.log('Загружаем профиль...')
    
    fetchUser('ivanov')
      .then(user => {
        console.log(`${user.name}: ${user.publicRepos} репозиториев`)
        return fetchRepos(user.login)    // возвращаем промис — цепочка продолжается
      })
      .then(repos => {
        const topRepo = repos.reduce((top, r) => r.stars > top.stars ? r : top)
        console.log(`Топ репо: ${topRepo.name} (${topRepo.stars} ⭐)`)
        return topRepo
      })
      .then(repo => console.log(`Язык: ${repo.lang}`))
      .catch(e => console.error('Ошибка:', e.message))
      .finally(() => console.log('Загрузка завершена'))
    
    // Promise.all — параллельная загрузка репозиториев и подписчиков
    Promise.all([
      fetchRepos('ivanov'),
      fetchFollowers('ivanov'),
    ])
      .then(([repos, followers]) => {
        console.log('Репозиториев:', repos.length)      // 3
        console.log('Подписчиков:', followers.length)   // 2
      })

    Промисы

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

    Открываешь GitHub — страница начинает загружать сразу всё: твой профиль, репозитории, уведомления, pull requests. JavaScript однопоточный, но браузер умеет делать сетевые запросы «в фоне». Вопрос: как получить результат, когда он придёт?

    Раньше использовали колбэки — и получали «ад колбэков»:

    // Callback hell — каждый уровень требует ещё одного колбэка
    getUser(id, function(user) {
      getRepos(user.login, function(repos) {
        getCommits(repos[0].id, function(commits) {
          render(user, repos, commits)  // вложенность растёт до бесконечности
        }, onError)
      }, onError)
    }, onError)

    Промис (Promise) — объект-обёртка для асинхронной операции. Он позволяет строить цепочки вместо пирамид вложенности.

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

  • «Функции» — колбэки, передача функций как аргументов
  • «try/catch» — обработка ошибок (в промисах — через .catch)
  • «Колбэки» — откуда растёт проблема, которую решают промисы
  • Три состояния промиса

  • pending — ожидание: операция ещё не завершена
  • fulfilled — успех: resolve(value) был вызван
  • rejected — ошибка: reject(error) был вызван
  • Состояние меняется только один раз и необратимо.

    Создание промиса

    const loadUser = new Promise((resolve, reject) => {
      // Асинхронная операция
      setTimeout(() => {
        const user = { id: 1, name: 'Иван' }
        if (user) {
          resolve(user)                   // передаём результат
        } else {
          reject(new Error('Не найден'))  // передаём ошибку
        }
      }, 300)
    })

    .then, .catch, .finally

    loadUser
      .then(user => {
        console.log('Пользователь:', user.name)
        return user.id  // возвращаем значение для следующего .then
      })
      .then(id => console.log('ID:', id))     // получаем то, что вернул предыдущий .then
      .catch(e => console.error('Ошибка:', e.message))   // ловим любую ошибку из цепочки
      .finally(() => console.log('Загрузка завершена'))  // всегда

    Ключевой момент: каждый .then() возвращает новый промис. Это позволяет строить цепочки.

    Цепочки промисов — вместо вложенности

    // Вместо вложенных колбэков — плоская цепочка
    getUser(1)
      .then(user => getRepos(user.login))       // возвращаем промис
      .then(repos => getCommits(repos[0].id))   // получаем его результат
      .then(commits => render(commits))
      .catch(e => showError(e.message))

    Promise.resolve и Promise.reject

    // Уже выполненный промис — удобно в тестах и дефолтных значениях
    const p1 = Promise.resolve({ id: 1, name: 'Тест' })
    p1.then(user => console.log(user.name))  // 'Тест'
    
    // Уже отклонённый промис
    const p2 = Promise.reject(new Error('Сервер недоступен'))
    p2.catch(e => console.log(e.message))   // 'Сервер недоступен'

    Promise.all — параллельные запросы

    Запускает несколько промисов одновременно и ждёт все:

    // Вместо последовательных 300мс + 200мс + 150мс = 650мс...
    // Параллельно: max(300, 200, 150) = 300мс
    const [user, repos, notifications] = await Promise.all([
      fetchUser(id),          // 300мс
      fetchRepos(id),         // 200мс
      fetchNotifications(id), // 150мс
    ])
    // Всё пришло через 300мс!

    Если хотя бы один промис отклоняется — весь Promise.all отклоняется немедленно.

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

    1. Не возвращают промис из .then():

    // Сломано — вложенный промис, а не цепочка:
    getUser(1)
      .then(user => {
        getRepos(user.id)  // нет return — следующий .then не получит repos!
      })
      .then(repos => console.log(repos))  // repos === undefined
    
    // Исправлено:
    getUser(1)
      .then(user => getRepos(user.id))  // return обязателен
      .then(repos => console.log(repos.length))

    2. Забывают .catch():

    // Сломано — ошибка "улетит" без обработки (UnhandledPromiseRejection):
    getUser(1)
      .then(user => render(user))
      // нет .catch — если getUser упадёт, узнаем только из логов Node.js
    
    // Исправлено:
    getUser(1)
      .then(user => render(user))
      .catch(e => showErrorMessage(e.message))

    3. Создают Promise там, где он уже есть:

    // Сломано — лишняя обёртка:
    function getUser(id) {
      return new Promise((resolve, reject) => {
        fetch(`/api/users/${id}`)
          .then(r => r.json())
          .then(resolve)
          .catch(reject)
      })
    }
    
    // Исправлено — fetch уже возвращает промис:
    function getUser(id) {
      return fetch(`/api/users/${id}`).then(r => r.json())
    }

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

  • Fetch API — возвращает промис, строишь цепочку
  • React: useEffect с промисами для загрузки данных
  • Node.js: чтение файлов, работа с БД — всё возвращает промисы
  • Promise.all — параллельная загрузка данных для дашборда
  • Примеры

    API GitHub: цепочка промисов и параллельная загрузка данных

    // Симуляция GitHub API
    function fetchUser(login) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (!login) return reject(new Error('Login обязателен'))
          resolve({ login, name: 'Иван Петров', publicRepos: 42, followers: 318 })
        }, 100)
      })
    }
    
    function fetchRepos(login) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve([
            { name: 'awesome-project', stars: 124, lang: 'JavaScript' },
            { name: 'my-blog', stars: 18, lang: 'TypeScript' },
            { name: 'utils-lib', stars: 67, lang: 'JavaScript' },
          ])
        }, 150)
      })
    }
    
    function fetchFollowers(login) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve([
            { login: 'anna', name: 'Анна Смирнова' },
            { login: 'boris', name: 'Борис Иванов' },
          ])
        }, 80)
      })
    }
    
    // Цепочка: получаем пользователя, потом его репозитории
    console.log('Загружаем профиль...')
    
    fetchUser('ivanov')
      .then(user => {
        console.log(`${user.name}: ${user.publicRepos} репозиториев`)
        return fetchRepos(user.login)    // возвращаем промис — цепочка продолжается
      })
      .then(repos => {
        const topRepo = repos.reduce((top, r) => r.stars > top.stars ? r : top)
        console.log(`Топ репо: ${topRepo.name} (${topRepo.stars} ⭐)`)
        return topRepo
      })
      .then(repo => console.log(`Язык: ${repo.lang}`))
      .catch(e => console.error('Ошибка:', e.message))
      .finally(() => console.log('Загрузка завершена'))
    
    // Promise.all — параллельная загрузка репозиториев и подписчиков
    Promise.all([
      fetchRepos('ivanov'),
      fetchFollowers('ivanov'),
    ])
      .then(([repos, followers]) => {
        console.log('Репозиториев:', repos.length)      // 3
        console.log('Подписчиков:', followers.length)   // 2
      })

    Задание

    Ты разрабатываешь клиент для API задачника (To-do app). Реализуй функцию `delay(ms)` — возвращает промис, который разрешается через `ms` миллисекунд. Реализуй функцию `simulateApi(endpoint)` — возвращает промис, который: - Через 100мс resolves с `{ data: endpoint, timestamp: Date.now() }` - Если `endpoint` содержит строку `"error"` — rejects с `new Error('API Error: ' + endpoint)` Используй цепочку `.then/.catch` для обработки результатов.

    Подсказка

    delay: setTimeout(resolve, ms) — передаём resolve напрямую. simulateApi: если endpoint.includes("error") вызови reject(new Error(...)), иначе resolve({ data: endpoint, timestamp: Date.now() }). В цепочке — обязательно return Promise внутри .then.

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