Промисы в JavaScript: полное руководство по асинхронности

Разбираемся с Promise в JavaScript: три состояния промисов, цепочки then/catch/finally, методы Promise.all и Promise.race. Практические примеры, типичные ошибки и лучшие практики работы с асинхронным кодом для разработчиков.

JSРазработка

6 мин

Что такое Promise

Promise — это объект, представляющий результат асинхронной операции, который может быть получен сейчас, в будущем или никогда. Это своего рода "обещание" вернуть результат, когда операция завершится.

Промис существует в одном из трёх состояний:

  • pending (ожидание) — начальное состояние, операция ещё выполняется

  • fulfilled (выполнено) — операция завершилась успешно

  • rejected (отклонено) — операция завершилась с ошибкой

Важно понимать, что промис может перейти из состояния pending только один раз — либо в fulfilled, либо в rejected. После этого состояние не меняется.

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

Промис создаётся с помощью конструктора Promise, который принимает функцию-исполнитель (executor). Эта функция получает два аргумента: resolve и reject.

const myPromise = new Promise((resolve, reject) => {
  // Асинхронная операция
  const success = true;
  
  if (success) {
    resolve('Операция выполнена успешно!');
  } else {
    reject('Произошла ошибка');
  }
});

Функция-исполнитель запускается сразу при создании промиса. Когда вы вызываете resolve(value), промис переходит в состояние fulfilled с результатом value. При вызове reject(error) промис переходит в rejected с ошибкой error.

Обработка результата: then, catch, finally

Для работы с результатом промиса используются методы then, catch и finally.

myPromise
  .then(result => {
    console.log('Успех:', result);
    return result.toUpperCase();
  })
  .catch(error => {
    console.error('Ошибка:', error);
  })
  .finally(() => {
    console.log('Операция завершена');
  });

Метод then принимает два необязательных аргумента: колбэк для успешного выполнения и колбэк для ошибки. На практике для обработки ошибок чаще используют catch, что делает код более читаемым.

Метод finally выполняется всегда, независимо от результата промиса. Это удобно для действий, которые нужно выполнить в любом случае, например, скрытие индикатора загрузки.

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

Одно из главных преимуществ промисов — возможность создавать цепочки асинхронных операций. Каждый then возвращает новый промис, что позволяет строить последовательности.

fetch('https://api.example.com/user/1')
  .then(response => response.json())
  .then(user => {
    console.log('Имя пользователя:', user.name);
    return fetch(`https://api.example.com/user/${user.id}/posts`);
  })
  .then(response => response.json())
  .then(posts => {
    console.log('Посты пользователя:', posts);
  })
  .catch(error => {
    console.error('Ошибка в цепочке:', error);
  });

В цепочке промисов ошибка на любом этапе "проваливается" до ближайшего catch. Это позволяет обрабатывать ошибки централизованно.

Практический пример: загрузка данных

Рассмотрим реальный сценарий — загрузку данных пользователя с сервера с обработкой различных ситуаций.

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    // Имитация задержки сети
    setTimeout(() => {
      const users = {
        1: { name: 'Алексей', role: 'developer' },
        2: { name: 'Мария', role: 'designer' }
      };
      
      const user = users[userId];
      
      if (user) {
        resolve(user);
      } else {
        reject(new Error('Пользователь не найден'));
      }
    }, 1000);
  });
}

// Использование
fetchUserData(1)
  .then(user => {
    console.log(`Привет, ${user.name}!`);
    console.log(`Ваша роль: ${user.role}`);
  })
  .catch(error => {
    console.error('Не удалось загрузить данные:', error.message);
  })
  .finally(() => {
    console.log('Запрос завершён');
  });

Promise.all и Promise.race

JavaScript предоставляет полезные статические методы для работы с множеством промисов.

Promise.all

Метод Promise.all принимает массив промисов и возвращает новый промис, который выполняется, когда выполнены все промисы в массиве. Если хотя бы один промис отклонён, весь Promise.all отклоняется.

const promise1 = Promise.resolve(3);
const promise2 = new Promise(resolve => setTimeout(() => resolve(42), 1000));
const promise3 = fetch('https://api.example.com/data').then(r => r.json());

Promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log('Все результаты:', results);
    // results будет массивом: [3, 42, {...данные из API}]
  })
  .catch(error => {
    console.error('Один из промисов завершился ошибкой:', error);
  });

Это удобно, когда нужно выполнить несколько независимых асинхронных операций и дождаться их всех.

Promise.race

Метод Promise.race возвращает промис, который выполняется или отклоняется, как только выполнится или отклонится первый из переданных промисов.

const slowPromise = new Promise(resolve => 
  setTimeout(() => resolve('Медленный'), 3000)
);
const fastPromise = new Promise(resolve => 
  setTimeout(() => resolve('Быстрый'), 1000)
);

Promise.race([slowPromise, fastPromise])
  .then(result => {
    console.log('Победитель:', result); // 'Быстрый'
  });

Это полезно для реализации таймаутов или выбора самого быстрого источника данных.

Promise.allSettled и Promise.any

Современные версии JavaScript добавили ещё два полезных метода.

Promise.allSettled

В отличие от Promise.all, метод Promise.allSettled ждёт завершения всех промисов независимо от результата. Возвращает массив объектов с информацией о каждом промисе.

const promises = [
  Promise.resolve('Успех'),
  Promise.reject('Ошибка'),
  Promise.resolve('Ещё успех')
];

Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`Промис ${index}: выполнен со значением ${result.value}`);
      } else {
        console.log(`Промис ${index}: отклонён с причиной ${result.reason}`);
      }
    });
  });

Promise.any

Метод Promise.any возвращает первый успешно выполненный промис. Если все промисы отклонены, возвращается отклонённый промис с агрегированной ошибкой.

const promises = [
  Promise.reject('Ошибка 1'),
  Promise.resolve('Успех'),
  Promise.reject('Ошибка 2')
];

Promise.any(promises)
  .then(result => {
    console.log('Первый успешный результат:', result); // 'Успех'
  })
  .catch(error => {
    console.error('Все промисы отклонены:', error);
  });

Типичные ошибки при работе с промисами

Забытый return в цепочке

Одна из частых ошибок — забыть вернуть промис из then, что разрывает цепочку.

// Неправильно
fetchUser()
  .then(user => {
    fetchPosts(user.id); // Забыли return!
  })
  .then(posts => {
    // posts будет undefined
    console.log(posts);
  });

// Правильно
fetchUser()
  .then(user => {
    return fetchPosts(user.id); // Возвращаем промис
  })
  .then(posts => {
    console.log(posts); // Теперь posts содержит данные
  });

Вложенные промисы вместо цепочек

Иногда разработчики создают вложенность промисов, что возвращает нас к проблеме callback-hell.

// Плохо — вложенность
fetchUser()
  .then(user => {
    fetchPosts(user.id)
      .then(posts => {
        fetchComments(posts[0].id)
          .then(comments => {
            console.log(comments);
          });
      });
  });

// Хорошо — цепочка
fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error(error));

Отсутствие обработки ошибок

Всегда добавляйте catch для обработки ошибок, иначе они могут остаться незамеченными.

// Опасно
fetchData().then(data => console.log(data));

// Безопасно
fetchData()
  .then(data => console.log(data))
  .catch(error => console.error('Произошла ошибка:', error));

От промисов к async/await

Промисы заложили фундамент для синтаксиса async/await, который делает асинхронный код ещё более похожим на синхронный.

// С промисами
function getUserInfo() {
  return fetchUser()
    .then(user => fetchPosts(user.id))
    .then(posts => {
      return { user, posts };
    });
}

// С async/await
async function getUserInfo() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  return { user, posts };
}

Однако понимание промисов критически важно, поскольку async/await — это синтаксический сахар над промисами, и знание их работы помогает эффективно использовать асинхронность в JavaScript.

Заключение

Промисы решили фундаментальную проблему JavaScript — элегантную работу с асинхронными операциями. Они предоставляют понятный интерфейс для управления последовательностью действий, обработки ошибок и композиции асинхронных операций.

Ключевые моменты для запоминания:

  • Промис находится в одном из трёх состояний и меняет его только один раз

  • Цепочки промисов решают проблему вложенности колбэков

  • Всегда обрабатывайте ошибки через catch

  • Используйте Promise.all для параллельного выполнения независимых операций

  • Возвращайте промисы из then для построения правильных цепочек

Глубокое понимание промисов — это основа для работы с современным асинхронным JavaScript и переход к async/await.

Хотите систематизировать свои знания? На платформе Кодик вы найдёте практические курсы по программированию с получением сертификата по окончании обучения.

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

Комментарии