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

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

Если вы когда-либо сталкивались с "адом колбэков" в JavaScript, то промисы — ваш спасательный круг. Это не просто синтаксический сахар, а фундаментальная концепция для работы с асинхронными операциями. В этой статье мы разберем промисы на практических примерах — от базовых до продвинутых — и научимся писать чистый, предсказуемый асинхронный код.

Что такое промис и зачем он нужен?

Промис (Promise) — это специальный объект в JavaScript, который представляет собой результат асинхронной операции, завершившейся успешно или с ошибкой. Вместо передачи колбэков в функцию, мы получаем объект, у которого можно вызвать методы .then(), .catch() и .finally().

Промис может находиться в одном из трех состояний: pending (ожидание), fulfilled (выполнено успешно), rejected (выполнено с ошибкой). После перехода в fulfilled или rejected состояние промиса больше не меняется.

Базовые примеры создания промисов

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

Давайте создадим свой первый промис, который имитирует загрузку данных:

const loadData = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = Math.random() > 0.3;
    
    if (success) {
      resolve({ data: 'Данные успешно загружены', status: 200 });
    } else {
      reject(new Error('Ошибка загрузки данных'));
    }
  }, 1000);
});

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

Промисы с реальными API

Вот практический пример с использованием Fetch API:

function getUserData(userId) {
  return fetch(`https://api.example.com/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    });
}

// Использование
getUserData(123)
  .then(user => console.log('Пользователь:', user))
  .catch(error => console.error('Ошибка:', error));

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

Одна из самых мощных возможностей промисов — возможность создавать цепочки:

function getUserPosts(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(user => {
      console.log('Найден пользователь:', user.name);
      return fetch(`/api/users/${userId}/posts`);
    })
    .then(response => response.json())
    .then(posts => {
      console.log(`Найдено ${posts.length} постов`);
      return posts;
    });
}

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

Продвинутые методы работы с промисами

Promise.all() — параллельное выполнение

Когда нужно выполнить несколько асинхронных операций параллельно и дождаться всех результатов:

const fetchUser = fetch('/api/user/1');
const fetchPosts = fetch('/api/posts');
const fetchComments = fetch('/api/comments');

Promise.all([fetchUser, fetchPosts, fetchComments])
  .then(responses => Promise.all(responses.map(r => r.json())))
  .then(([user, posts, comments]) => {
    console.log('Все данные загружены:', { user, posts, comments });
  })
  .catch(error => console.error('Одна из загрузок завершилась ошибкой:', error));

Promise.race() — гонка промисов

Полезно, когда нужно получить результат первого выполнившегося промиса:

const timeout = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('Таймаут!')), 5000);
});

const apiRequest = fetch('/api/data');

Promise.race([apiRequest, timeout])
  .then(response => console.log('Данные получены вовремя'))
  .catch(error => console.error('Ошибка:', error.message));

Promise.allSettled() — все результаты

Получить результаты всех промисов, независимо от их успешности:

Promise.allSettled([
  Promise.resolve('Успех 1'),
  Promise.reject(new Error('Ошибка')),
  Promise.resolve('Успех 2')
])
.then(results => {
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Промис ${index}:`, result.value);
    } else {
      console.log(`Промис ${index} отклонен:`, result.reason.message);
    }
  });
});

Async/await — синтаксический сахар над промисами

Современный способ работы с промисами, который делает код более читаемым:

async function loadUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    
    if (!response.ok) {
      throw new Error(`Ошибка HTTP: ${response.status}`);
    }
    
    const user = await response.json();
    const postsResponse = await fetch(`/api/users/${userId}/posts`);
    const posts = await postsResponse.json();
    
    return { user, posts };
  } catch (error) {
    console.error('Ошибка загрузки:', error);
    throw error;
  } finally {
    console.log('Загрузка данных пользователя завершена');
  }
}

// Использование
loadUserData(123)
  .then(data => console.log('Данные:', data))
  .catch(error => console.error('Финальная ошибка:', error));

Функция с async всегда возвращает промис. Ключевое слово await можно использовать только внутри async-функций.

Распространенные ошибки и лучшие практики

  • Не забывайте обрабатывать ошибки: Всегда добавляйте .catch() или используйте try/catch с async/await
  • Избегайте вложенных промисов: Используйте цепочки или async/await для плоской структуры
  • Не создавайте промисы без необходимости: Если операция синхронная, не оборачивайте ее в промис
  • Используйте Promise.all для параллельных операций: Не делайте последовательные await для независимых операций

FAQ — Часто задаваемые вопросы

В чем разница между Promise.all() и Promise.allSettled()?

Promise.all() завершается с ошибкой, если хотя бы один промис отклонен. Promise.allSettled() всегда ждет завершения всех промисов и возвращает массив с результатами каждого.

Можно ли отменить промис?

Стандартные промисы JavaScript не поддерживают отмену. Для этого можно использовать AbortController с Fetch API или сторонние реализации отменяемых промисов.

Чем async/await лучше обычных промисов?

Async/await делает асинхронный код более читаемым и похожим на синхронный. Однако это всего лишь синтаксический сахар — под капотом все равно работают промисы.

Что такое "обещание" в контексте промисов?

Промис (Promise) переводится как "обещание". Это действительно обещание JavaScript предоставить результат операции в будущем — успешный или с ошибкой.

Как обрабатывать несколько ошибок в цепочке промисов?

Можно использовать несколько .catch() в цепочке или один общий в конце. Каждый .catch() может обработать ошибку и либо вернуть значение, либо пробросить ошибку дальше.