Промисы в JavaScript: от боли коллбэков к элегантному коду. Практические примеры на каждый день

Промисы в JavaScript: от боли коллбэков к элегантному коду. Практические примеры на каждый день

Если вы до сих пор путаетесь в цепочках .then() и .catch(), а слово "асинхронность" вызывает легкую панику — вы не одиноки. Я сам через это прошел. Давайте разберем промисы на реальных примерах так, чтобы они перестали быть магией и стали вашим основным инструментом.

Что такое "промисы js примеры" и почему это нужно?

Промис (Promise) — это не просто замена коллбэкам, а принципиально другой способ организации асинхронного кода. Представьте, что вы даете JavaScript "расписку" на выполнение задачи. Эта расписка может быть: выполнена (fulfilled), отклонена (rejected) или находиться в ожидании (pending). Именно эта метафора помогла мне объяснить промисы стажеру в прошлом году.

Важный факт: Промисы появились в ES6 (2015), но до сих пор многие разработчики используют их поверхностно, упуская мощные возможности вроде Promise.allSettled() или комбинации с async/await.

Критерии выбора подхода (Таблица из 5 параметров)

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

ПараметрПромисыКоллбэкиAsync/Await
ЧитаемостьХорошаяПлохая (ад коллбэков)Отличная
Обработка ошибокЦентрализованная (.catch)РазрозненнаяTry/Catch
Поддержка браузерамиES6+ (почти все)ВездеES2017+
КомпозицияОтличная (Promise.all)СложнаяХорошая
Кривая обученияСредняяНизкаяНизкая (после промисов)

Топ-3 сценария использования на рынке

1. Цепочка последовательных запросов

Классика: получить пользователя, потом его заказы, потом детали заказа.

2. Параллельное выполнение независимых задач

Загрузка данных для дашборда: статистика, уведомления, фиды.

3. Обработка с таймаутом

"Если API не ответил за 5 секунд — показываем заглушку".

Детальное 10-балльное сравнение методов

  1. Базовый синтаксис: new Promise((resolve, reject) => { ... })
  2. Состояния: pending, fulfilled, rejected — только 1 переход!
  3. Цепочки: .then() возвращает новый промис
  4. Ошибки: .catch() ловит ошибки во всей цепочке выше
  5. Финализация: .finally() выполнится в любом случае
  6. Параллелизм: Promise.all([p1, p2]) — ждем все
  7. Гонки: Promise.race([p1, p2]) — первый ответ
  8. Все с результатами: Promise.allSettled() — даже с ошибками
  9. Любой успешный: Promise.any() — первый успех
  10. Синтаксический сахар: async/await над промисами

Мой личный выбор и почему

Я использую гибридный подход. Для простых операций — async/await для читаемости. Для сложной логики параллелизма — промисы с Promise.allSettled(). Вот реальный пример из проекта электронной коммерции:

// Загрузка данных для страницы товара
async function loadProductPage(productId) {
  try {
    // Параллельно грузим основное и доп. данные
    const [product, reviews, recommendations] = await Promise.all([
      fetchProduct(productId),
      fetchReviews(productId),
      fetchRecommendations(productId)
    ]);
    
    // А вот это уже последовательно, т.к. нужен ID пользователя
    const userData = await fetchUserData();
    const personalizedPrice = await calculatePrice(product, userData);
    
    return { product, reviews, recommendations, personalizedPrice };
  } catch (error) {
    // Одна точка обработки ошибок!
    console.error('Ошибка загрузки страницы:', error);
    showErrorUI();
    // Возвращаем промис с заглушкой
    return Promise.resolve(getFallbackData());
  }
}

Экспертный совет: Всегда возвращайте что-то из .catch(), даже если это заглушка. Иначе следующий .then() получит undefined и сломается.

Руководство по внедрению

Шаг 1: Замена коллбэков на промисы

Оберните старый API:

function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

Шаг 2: Построение цепочек

Не делайте "пирамиду ужаса":

// ПЛОХО
getUser().then(user => {
  getOrders(user.id).then(orders => {
    // ... ещё глубже
  });
});

// ХОРОШО
getUser()
  .then(user => getOrders(user.id))
  .then(orders => processOrders(orders))
  .catch(error => console.error(error));

Шаг 3: Обработка ошибок

Один .catch() в конце цепочки ловит ВСЕ ошибки выше. Это мощно!

Ключевые выводы

  • Промисы — это контейнеры для будущих значений, а не сами значения
  • Цепочки .then() всегда возвращают новый промис
  • Используйте Promise.all для параллельных НЕзависимых операций
  • Async/await — это синтаксический сахар над промисами, а не замена
  • Никогда не забывайте про .catch() — необработанные обещания "молча умирают"

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

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

Promise.all немедленно отклоняется, если любой промис отклонен. Promise.allSettled ждет завершения ВСЕХ промисов и возвращает массив результатов с статусами.

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

Нет, стандартный промис нельзя отменить. Для этого нужны кастомные реализации или AbortController с fetch.

Что возвращает async функция?

Всегда возвращает промис! Даже если вы написали return 42, функция вернет Promise.resolve(42).

Почему .then(() => {}) выполняется после синхронного кода?

Потому что промисы попадают в микрозадачи (microtask queue), которые обрабатываются после синхронного кода, но до макрозадач (setTimeout).

Как избежать "Memory Leaks" с промисами?

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

Полезные ресурсы 2024-2025: