Вивчай

Promises

JavaScript Promises — офіціант у ресторані несе одночасно кілька тарілок обслуговуючи багато столиківJavaScript Promises — офіціант у ресторані несе одночасно кілька тарілок обслуговуючи багато столиків

До цього моменту весь наш JavaScript був синхронним — кожен рядок виконувався по черзі. Але реальні застосунки потребують завантаження даних з сервера, читання файлів, очікування відповіді користувача. Все це займає час, і JavaScript має елегантний механізм для роботи з такими операціями — Promises.


Навіщо потрібна асинхронність?

Уяви: ти завантажуєш список користувачів з сервера. Запит може тривати 1-3 секунди. Якщо JavaScript буде чекати відповіді — вся сторінка "заморозиться": кнопки не працюватимуть, анімації зупиняться.

// Проблема: синхронний код блокує все
const data = fetchFromServer("https://api.example.com/users"); // ⏳ 2 секунди чекання
console.log(data); // Все заморожено, поки чекаємо!

JavaScript вирішує це асинхронністю — запит відправляється, а код продовжує виконуватися. Коли відповідь приходить, викликається callback.


Event Loop — як працює асинхронність

JavaScript — однопоточна мова. Він має лише один потік виконання. Але завдяки event loop може обробляти асинхронні операції.

Event Loop: Call Stack, Web APIs, Task QueueEvent Loop: Call Stack, Web APIs, Task Queue

КомпонентЩо робить
Call StackВиконує функції по черзі (LIFO)
Web APIsОбробляють таймери, мережеві запити, DOM події
Task QueueЧерга callback-ів, готових до виконання
Event LoopПереміщує задачі з черги в call stack, коли він порожній
console.log("1: Початок");

setTimeout(() => {
  console.log("2: Таймер спрацював");
}, 0); // навіть з 0 мс — це асинхронна операція!

console.log("3: Кінець");

// Результат:
// "1: Початок"
// "3: Кінець"
// "2: Таймер спрацював"
Інфо

setTimeout(() => {}, 0) не виконується миттєво! Callback потрапляє в Task Queue і чекає, поки call stack звільниться. Тому "3: Кінець" завжди виведеться раніше за "2: Таймер спрацював".


Callbacks — старий підхід

До Promises для асинхронного коду використовували callbacks — функції, які передавались як аргументи:

function loadUser(id, onSuccess, onError) {
  setTimeout(() => {
    if (id > 0) {
      onSuccess({ id, name: "Олексій" });
    } else {
      onError("Невірний ID");
    }
  }, 1000);
}

loadUser(
  1,
  (user) => console.log("Користувач:", user.name),
  (error) => console.log("Помилка:", error)
);

Проблема виникає, коли потрібно виконати кілька операцій послідовно — Callback Hell:

loadUser(1, (user) => {
  loadPosts(user.id, (posts) => {
    loadComments(posts[0].id, (comments) => {
      loadReplies(comments[0].id, (replies) => {
        console.log(replies); // 😵 "Пірамідка смерті"
      }, handleError);
    }, handleError);
  }, handleError);
}, handleError);
Увага

Callback Hell (або "пірамідка смерті") робить код нечитабельним та складним для підтримки. Promises вирішують цю проблему через chaining.


Що таке Promise?

Promise (обіцянка) — це об'єкт, який представляє результат асинхронної операції. Він може бути в одному з трьох станів:

СтанОпис
pendingПочатковий стан, операція ще виконується
fulfilledОперація успішно завершилась (є результат)
rejectedОперація завершилась з помилкою

Після переходу у fulfilled або rejected стан більше не змінюється.


Створення Promise

const myPromise = new Promise((resolve, reject) => {
  // Асинхронна операція
  setTimeout(() => {
    const success = true;

    if (success) {
      resolve("Дані отримано!"); // fulfilled
    } else {
      reject("Щось пішло не так"); // rejected
    }
  }, 1000);
});
  • resolve(value) — викликається при успіху, передає результат
  • reject(error) — викликається при помилці

Використання: then, catch, finally

.then() — обробка успіху

myPromise.then((result) => {
  console.log(result); // "Дані отримано!"
});

.catch() — обробка помилки

myPromise.catch((error) => {
  console.log("Помилка:", error);
});

.finally() — виконається завжди

myPromise.finally(() => {
  console.log("Операція завершена (успішно чи ні)");
});

Повний приклад

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: "Олексій", age: 25 });
      } else {
        reject(new Error("ID має бути додатнім числом"));
      }
    }, 1000);
  });
}

fetchUser(1)
  .then((user) => console.log("Користувач:", user.name))
  .catch((error) => console.log("Помилка:", error.message))
  .finally(() => console.log("Запит завершено"));

// Через 1 секунду:
// "Користувач: Олексій"
// "Запит завершено"

Chaining — ланцюжки промісів

Головна перевага Promises — можливість chaining (ланцюжків). Кожен .then() повертає новий Promise:

fetchUser(1)
  .then((user) => {
    console.log("Користувач:", user.name);
    return user.id; // Передаємо ID далі
  })
  .then((userId) => {
    console.log("Завантажуємо пости для ID:", userId);
    return fetchPosts(userId); // Повертаємо Promise
  })
  .then((posts) => {
    console.log("Отримано постів:", posts.length);
  })
  .catch((error) => {
    // Ловить помилку з БУДЬ-ЯКОГО .then() вище
    console.log("Помилка:", error.message);
  });
Порада

Один .catch() в кінці ланцюжка ловить помилки з усіх попередніх .then(). Не потрібно обробляти помилки на кожному кроці.


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

Коли потрібно виконати кілька незалежних промісів одночасно:

const promise1 = fetchUser(1);      // 1 сек
const promise2 = fetchUser(2);      // 1 сек
const promise3 = fetchUser(3);      // 1 сек

// Всі три запити виконуються паралельно!
Promise.all([promise1, promise2, promise3])
  .then((users) => {
    // users — масив результатів [user1, user2, user3]
    console.log("Всі користувачі:", users);
  })
  .catch((error) => {
    // Якщо ХОЧА Б ОДИН промис rejected — весь Promise.all rejected
    console.log("Помилка:", error.message);
  });

// Загальний час: ~1 сек (не 3!)
Увага

Promise.all() "падає" при першій помилці. Якщо потрібно отримати результати всіх промісів (навіть з помилками), використовуй Promise.allSettled().


Promise.race() — перший результат

Повертає результат промісу, який завершився першим:

const fast = new Promise((resolve) => setTimeout(() => resolve("Швидкий"), 100));
const slow = new Promise((resolve) => setTimeout(() => resolve("Повільний"), 2000));

Promise.race([fast, slow]).then((result) => {
  console.log(result); // "Швидкий"
});

Корисно для тайм-аутів:

function fetchWithTimeout(url, ms) {
  const fetchPromise = fetch(url);
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Тайм-аут!")), ms)
  );
  return Promise.race([fetchPromise, timeout]);
}

Promise.allSettled()

На відміну від Promise.all(), чекає завершення всіх промісів:

const promises = [
  fetchUser(1),    // успіх
  fetchUser(-1),   // помилка
  fetchUser(3),    // успіх
];

Promise.allSettled(promises).then((results) => {
  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log("Успіх:", result.value);
    } else {
      console.log("Помилка:", result.reason.message);
    }
  });
});

Типові помилки

1. Забули повернути значення в .then()

// ❌ Неправильно — result буде undefined
fetchUser(1)
  .then((user) => {
    fetchPosts(user.id); // забули return!
  })
  .then((posts) => {
    console.log(posts); // undefined 😱
  });

// ✅ Правильно
fetchUser(1)
  .then((user) => {
    return fetchPosts(user.id);
  })
  .then((posts) => {
    console.log(posts); // масив постів
  });

2. Забули .catch()

// ❌ Помилка "проковтнеться" без сліду
fetchUser(-1).then((user) => console.log(user));

// ✅ Завжди додавай .catch()
fetchUser(-1)
  .then((user) => console.log(user))
  .catch((error) => console.error("Помилка:", error.message));

Практика

Створи функцію, яка симулює завантаження даних:

function loadData(name, delay, shouldFail = false) {
  return new Promise((resolve, reject) => {
    console.log(`Завантаження ${name}...`);
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error(`Не вдалося завантажити ${name}`));
      } else {
        resolve(`${name}: дані отримано`);
      }
    }, delay);
  });
}

// Послідовне виконання
loadData("Профіль", 1000)
  .then((result) => {
    console.log(result);
    return loadData("Пости", 800);
  })
  .then((result) => {
    console.log(result);
    return loadData("Коментарі", 600);
  })
  .then((result) => console.log(result))
  .catch((error) => console.error(error.message));

// Паралельне виконання
Promise.all([
  loadData("Профіль", 1000),
  loadData("Пости", 800),
  loadData("Коментарі", 600),
])
  .then((results) => console.log("Все завантажено:", results))
  .catch((error) => console.error(error.message));

Підсумок

  • Асинхронність дозволяє не блокувати виконання коду під час очікування
  • Event Loop переміщує готові callback-и з черги в call stack
  • Promise — об'єкт з трьома станами: pending, fulfilled, rejected
  • .then() — обробка успіху, .catch() — обробка помилки, .finally() — завжди
  • Chaining замінює callback hell плоским ланцюжком
  • Promise.all() — паралельне виконання (падає при першій помилці)
  • Promise.race() — результат найшвидшого промісу
  • Promise.allSettled() — чекає всіх, повертає статус кожного

Що далі?

У наступному уроці — async/await, який робить асинхронний код ще простішим та читабельнішим.

Інфо

Корисні посилання:

Відео: