Що таке асинхронний код
Як ви можете памʼятати, JS є однопоточною мовою програмування і це означає що в нас є тільки один основний потік у якому рядок за рядком виконується написаний нами код. Але, оскільки JS використовується у якомусь оточені, де існує безліч різноманітних API від безлічі різноманітних подій та запитів до серверу у браузері до запитів до файлової системи чи бази даних у Node, з якими неможливо спілкуватися синхронно. Точніше можливо, але вам це не сподобається. Давайте уявімо собі ситуацію, коли ви, як користувач месенджеру відправляєте повідомлення, але для того щоб почати друкувати нове, вам буде необхідно дочекатися відповіді від серверу, а до того моменту весь ваш застосунок буде повністю неактивним. Більш того, щоб дізнатися чи є для вас якісь нові повідомлення, треба буде відправляти новий запит до серверу та питати, чи є щось новеньке в чаті. Виглядає не дуже зручно та сучасно. Погодьтесь, що куди краще було б відправляти запит та очікувати відповідь від сервера у фоні, щоб мати можливість продовжити користування застосунком не чекаючі що сервер відповість, а коли відповідь прийде — обробити її та побачити зміни. Але як це зробити, якщо потік тільки один? У цьому нам допоможе асинхронний код.
Асинхронний код — це код, який виконується поза основним потоком виконання програми та не блокує його. На відміну від синхронного коду, який виконується послідовно, крок за кроком асинхронний код може виконуватися паралельно з іншими операціями. Таку можливість паралельного виконання нам надає середовище виконання нашого коду.
Таким чином, процес виконання не блокує роботу програми (як це відбувається в синхронному програмуванні), і вона може продовжувати виконувати інші завдання. Відповідно, це збільшує ефективність програми і робить її більш відмовостійкою. У синхронному програмуванні, якщо одна операція займає надто багато часу, вся програма блокується і стає недоступною для користувачів. У той час як асинхронне програмування дає змогу виконувати тривалі операції у фоновому режимі, без блокування головного потоку програми, тим самим забезпечуючи швидкодію програми. Саме це нам і необхідно для того щоб зробити наш застосунок більш зручним.
Чудово! Але як дізнатися коли прийде відповідь, щоб обробити її? Для цього у JS є декілька інструментів.
Функції зворотного виклику
Функції зворотного виклику (callback functions) — це функції, які передаються як аргументи іншим функціям. Ці функції призначені для виконання певних дій після завершення виконання іншої функції або певних подій. Функції зворотного виклику використовуються для обробки асинхронних операцій, подій, обробки даних та інших сценаріїв, коли необхідно виконати певні дії після завершення певної операції.
Для того, щоб краще зрозуміти, що це таке, давайте спочатку розглянемо, як працюють синхронні функції зворотного виклику. Уявімо собі, що ви сиділи у кафе разом із своїми батьками і вже настала пора їхати до дому. Ви викликали таксі для батьків, але ви все одно хвилюєтеся за них і хочете впевнитися, що вони дістались до дому без пригод. Ви знаєте що шлях до дому займає приблизно 15 хвилин, але по дорозі вони могли трохи затриматися і у вас є два варіанти. Або дзвонити їм через 15 хвилин з якимось інтервалом до тих пір поки вони не опиняться вдома, або попрохати їх, щоб вони вам передзвонили, коли будуть вдома. Давайте опишемо це у вигляді коду.
function letsGoHome(cbFunction) {
console.log('Call a taxi');
console.log('On the road...');
cbFunction();
}
function callback() {
console.log('We are at home!');
}
letsGoHome(callback);
Але, насправді, шлях до дому не відбувається миттєво та займає якийсь час. Щоб імітувати цю поведінку, скористаємося API для створення таймеру, який надає нам оточення у якому виконується наш код.
Таймери
Таймери — це глобальні методи, які дозволяють виконувати певні дії відкладені у часі. Таймери корисні для асинхронних завдань, планування виконання коду та керування часовими інтервалами. Існують два основні методи для роботи з таймерами — setTimeout та setInterval. Є ще setImmediate, але він працює тільки у Node, тому ми його зараз розглядати не будемо.
setTimeout / clearTimeout
setTimeout — це метод, який дозволяє запланувати виконання блоку коду через певний період часу. Виглядає він наступним чином.
setTimeout(callback, delay, arg1, arg2, ...);
callback
— функція, яка має бути виконана після закінчення зазначеної затримки
delay
— затримка в мілісекундах (1000 мілісекунд = 1 секунда)arg1, arg2, ...
— додаткові аргументи, які передадуть функціїcallback
function saySmth(phrase, name) {
console.log(`${phrase} ${name}!`);
}
setTimeout(saySmth, 1000, 'Hello', 'John');
Наведена вище функція saySmth
, відкладена не менш ніж на 1 секунду та приймає два аргументи: phrase
та name
, а виклик setTimeout
передає їй значення Hello
та John
.
Зверніть увагу! Ми не викликаємо функцію, тільки передаємо її.
setTimeout(saySmth(), 1000); // неправильно!
Це не працює, оскільки setTimeout
очікує посилання на функцію. А saySmth()
запускає функцію, і результат її виконання передається setTimeout
. У нашому випадку результат saySmth()
є undefined
, тому нічого не планується.
Іноді виникає необхідність у скасуванні вже створеного таймеру. Для цього існує окремий метод clearTimeout
. Це метод приймає лише один аргумент — ідентифікатор таймера, який потрібно скасувати. Ідентифікатор таймера — це число, яке буде повернуто методом setTimeout()
при його виклику.
clearTimeout(timeoutID);
timeoutID — це ідентифікатор таймера, який потрібно скасувати
function saySmth(phrase, name) {
console.log(`${phrase} ${name}!`);
}
const timerId = setTimeout(saySmth, 1000, 'Hello', 'John');
// якщо ми вирішимо скасувати виконання функції, ми викликаємо clearTimeout з ідентифікатором таймера
clearTimeout(timerId);
setInterval / clearInterval
Іноді у нас виникає необхідність не просто відкласти виклик функції, а повторювати виклик з якимось інтервалом. Для цього існує метод setInterval. Виглядає цей метод наступним чином:
setInterval(callback, delay, arg1, arg2, ...);
callback
— функція, яка має бути виконана після закінчення зазначеної затримки
delay
— затримка в мілісекундах (1000 мілісекунд = 1 секунда) яка є інтервалом між викликамиarg1, arg2, ...
— додаткові аргументи, які передадуть функціїcallback
function saySmth(phrase, name) {
console.log(`${phrase} ${name}!`);
}
// Запускаємо функцію saySmth кожну секунду (кожні 1000 мілісекунд)
setInterval(saySmth, 1000, 'Hello', 'John');
Так само, як і для очишення таймауту, є метод clearInterval — який використовується для зупинки виконання інтервалу, раніше створеного за допомогою setInterval. clearInterval() приймає один аргумент — ідентифікатор таймера, який потрібно скасувати.
clearInterval(intervalId);
intervalId — це ідентифікатор інтервалу, який потрібно зупинити. Це значення, яке буде повернуто функцією setinterval під час створення інтервалу.
function saySmth(phrase, name) {
console.log(`${phrase} ${name}!`);
}
const intervalId = setInterval(saySmth, 1000, 'Hello', 'John');
clearInterval(intervalId);
Рекурсивний setTimeout
іноді виникає необхідність дочекатися кінця виконання функції для того, щоб запланувати новий таймер. У такому випадку нам допоможить рекурсивний виклик setTimeout
let timerId = setTimeout(function tick() {
// some code here
timerId = setTimeout(tick, 2000); // (*)
}, 2000);

Загальний огляд механізму EventLoop. Callback hell
Загальний огляд механізму EventLoop
Механізм Event Loop (цикл подій) є важливою частиною асинхронної моделі виконання коду і використовується як у браузерах, так і у Node.js, але з багатьма відмінностями, які ми не будемо розглядати у контексті цього курсу. Цей механізм дає змогу ефективно та передбачувано обробляти та керувати подіями та виконанням завдань в асинхронному середовищі. Для розуміння Event loop та послідовності виконання нашого асинхронного коду, нам потрібно розглянути три основні складові:
- Stack
- Callback (Event) Queue
- Event loop
Якщо зі стеком ми вже зустрічалися та памʼятаємо, що це структура LIFO, то черга — це схожа структура для зберігання готових асинхронних коллбеків, яка має тип FIFO. Що ж тоді таке Event loop? Це механізм, який циклічно перевіряє чергу, забезпечуючи виконання коду, коли готові асинхронні завдання. Event Loop працює в нескінченному циклі, який виконується доти, доки в черзі подій є елементи для обробки. Також дуже важливим є те, що він працює не блокувальним чином, що означає, що він не зупиняє виконання програми, чекаючи завершення асинхронних операцій. Замість цього він періодично перевіряє чергу і виконує доступні завдання. У кожній ітерації циклу Event Loop виконує наступні дії:
- Коли API повідомляє, що коллбеки вже готові до виклику, вони переносяться у чергу
- Далі іде перевірка чи пустий зараз Stack
- Після цього перевіряє, чи є коллбеки в черзі
- Якщо Stack пустий і є коллбеки в черзі, то бере перший коллбек з черги та переносить в Stack для виконання

Звісно це не повний опис цього механізму і він містить ще багато речей, частину яких ми розглянемо на наступному занятті. Наразі нам необхідно зрозуміти концепцію, тому що вона є ключовою в розробці асинхронних додатків, і розуміння роботи цього механізму важливе для всіх розробників, які працюють з асинхронними середовищами.
Callback hell
Коллбеки чудові, але з ними є одна проблема. Давайте з вами напишемо програму для замовлення в кафе.
const menu = {
americano: [
{ item: '🍔', time: 3000 },
{ item: '🍟', time: 1500 },
{ item: '🥤', time: 500 },
],
italiano: [
{ item: '🍕', time: 1500 },
{ item: '🥗', time: 1500 },
{ item: '🍷', time: 500 },
],
breakfast: [
{ item: '🥪', time: 1000 },
{ item: '🍳', time: 2500 },
{ item: '☕', time: 1000 },
],
};
const order = (dishes, onComplete) => {
console.log(`Start cooking order ...`);
const orderResult = [];
const getResultData = (dish, index) => {
orderResult[index] = dish;
if (orderResult.filter(Boolean).length === dishes.length) onComplete(orderResult);
};
const cookFood = ({ name: item, time }, index, onDishComplete) => {
// const condition = +Math.random().toFixed();
const condition = 1;
setTimeout(() => {
const dish = condition ? { status: 'fulfilled', value: item } : { status: 'rejected', reason: `${item} failed` };
onDishComplete(dish, index);
}, time);
};
dishes.forEach((item, index) => cookFood(item, index, getResultData));
};
order(menu.americano, console.log);
Ми можемо бачити що це дуже важко читати через вкладеність функцій. Насправді це є простим прикладом і в реальному коді можна було б побачити більш вкладені виклики. Далі ми побачимо як ця проблема була вирішена в JavaScript.
Promise
Що таке Promise
Promise
— це концепція в програмуванні, яка використовується для керування асинхронними операціями. Promise
являє собою об’єкт, що представляє результат виконання асинхронної операції, яка може завершитися успішно або з помилкою. Навідміну від колбеків, проміси забезпечують кращий та більш зрозуміліший спосіб роботи з асинхронними операціями.
// Callback
asyncFunction(a, b, result => {
console.log(result);
});
// Promise
asyncFunction(a, b)
.then(result => {
console.log(result);
});
У порівнянні зі зворотним викликом як продовженням, проміси мають наступні переваги:
- Відсутність інверсії керування: подібно до синхронного коду, функції на основі обіцянок повертають результати, а не продовжують / керують виконанням через зворотні виклики. Тобто, контроль залишається за тим, хто викликає функцію.
- Ланцюжок простіше: якщо виклик функції повертає
Promise
, то ви можете об’єднувати виклики методуthen()
у ланцюжок, що також спрощує керування процессом. - Поєднання асинхронних викликів: трохи простіше, оскільки у вас є дані (об’єкти
Promise
), з якими ви можете працювати. - Обробка помилок: Як ми побачимо пізніше, обробка помилок у
Promise
простіша, тому що, знову ж таки, немає інверсії управління. Крім того, виключення та асинхронні помилки обробляються однаково.
З чого складається Promise
Якщо коротко, то Promise
— це спеціальний об’єкт, який містить свій стан. Його стан можє мати одне з трьох значень:
pending
— на початку виконанняfulfilled
— у разі успішного завершенняrejected
— у разі помилки
Коли ми створюємо новий проміс за допомогою конструктору new Promise
, то у якості аргументу передаємо йому функцію. Ця функція зветься executor і запускається автоматично, коли створюється Promise
. Всередині вона містить код, що коли-небудь створить якийсь результат та отримує два аргументи resolve
і reject
. Це колбеки, які надає сам JavaScript.
Коли Promise
отримує результат, зараз чи пізніше — не важливо, він має викликати один із цих колбеків:
resolve(value)
— якщо робота завершилася успішно, деvalue
— результат виконання.reject(error)
— якщо сталася помилка, деerror
— об’єкт помилки.
Отже, коли створюється Promise
, автоматично запускається executor, стан у цей момент — pending
, коли виконання роботи завершиться, буде викликано коллбек resolve
або reject
та стан відповідно зміниться на fulfilled
або rejected
.
Якщо ще простіше, то executor за підсумком переводить Promise
в один із двох станів.
Виглядає це наступним чином:Помилка
new Promise(
function(resolve, reject) {
···
if (···) {
resolve(value); // success
} else {
reject(reason); // failure
}
}
);
Дуже важливим моментом є те, що стан Promise після завершення є незмінним. Тобто Promise може бути виконаний лише один раз, після чого він залишається виконаним. Наступні спроби виконати Promise не мають жодного ефекту.
“Споживання” промісу
Як споживач Promise
, ви отримуєте повідомлення про виконання або відхилення через реакції — зворотні виклики, які ви реєструєте за допомогою методів then()
і catch()
:
- Метод
then()
приймає дві функції зворотнього виклику, перша спрацьовує у випадку вдалого завершення обіцянки, а друга — у випадку завершення з помилкою. - Метод
catch()
спрацьовую виключно у випадку завершення обіцянки з помилкою. Це просто зручніша і рекомендована альтернатива.
Помилка
promise
.then(value => { /* fulfillment */ }, error => { /* rejection */ })
.catch(error => { /* rejection */ });
Проміси дуже корисні для асинхронних функцій з одноразовим результатом, тому що після того, як проміс виконаний, він більше не змінюється. Крім того, ніколи не виникає race condition, тому що не має значення, чи ви викликаєте then()
або catch()
до або після того, як проміс буде виконано:
catch()
і його зворотний виклик
Різниця між then()
і catch()
полягає в тому, що останній спрацьовує при відхиленні(reject), а не при успішному виконанні. Однак обидва методи однаково перетворюють дії своїх колбеків на Promise
. Наприклад, у наведеному нижче коді значення, повернуте функцією зворотного виклику стає значенням виконання:
new Promise((_, reject) => reject(new Error('My error')))
.catch(() => 'default value')
.then(console.log);
finally
Окрім вже розглянутих методів then()
та catch()
відносно нещодавно зʼявився метод finally()
. Цей приймає функцію колбек, яка не отримає жодного аргумента. Він виконується завжди, незалежно від того з яким статусом завершився Promise
та незважаючи на значення, що повертаються методами then()
та/або catch()
. І навпаки.
Зазвичай finally()
використовують наприкінці для того щоб виконати якісь дії незалежно від результату виконання, наприклад — прибрати індикатор завантаження файлу, це потрібно як у разі успіху так і при помилці.
new Promise(resolve => resolve('fulfilled'))
.then(result => {
console.log(`then ${result}`);
})
.catch(error => {
console.log(`catch ${error}`);
})
.finally(() => {
console.log('finally');
})
Ланцюг споживачів
Як ми вже знаємо then()
та catch()
завжди повертають Promise
. Це дозволяє нам створювати довгі ланцюжки викликів методів:
function asyncFunc() {
return asyncFuncA()
.then(result => {
// ···
return asyncFuncB();
})
.then(result => {
// ···
return result;
})
.then(result => {
// ···
return asyncFuncC();
});
}
У якомусь сенсі then() є асинхронною версією синхронної крапки з комою бо then() виконує дві асинхронні операції послідовно. Крапка з комою виконує дві синхронні операції послідовно. Ми також можемо додати catch() і дозволити йому обробляти декілька джерел помилок одночасно:
asyncFuncA()
.then(result => {
// ···
return asyncFunctionB();
})
.then(result => {
// ···
})
.catch(error => {
// Помилка: обробка помилок asyncFuncA(), asyncFuncB()
// та будь-які виключення, згенеровані у попередніх викликах
});
А також ви можете обробляти помилки після кожного виклику then().
Необроблені помилки
Може виникнути така ситуація, коли помилка не буде оброблена, наприклад, просто забули додати catch()
в кінець ланцюжка. У разі помилки виконання повинне перейти до найближчого обробника помилок. Але, якщо і такого обробника немає то помилка ніби «застряє», її нікому обробити. Якщо відбувається помилка, і відсутній її обробник, то генерується подія unhandledrejection
, і відповідний об’єкт event
містить інформацію про помилку. Якщо і такого обробника немає, движок відстежує такі ситуації і генерує в цьому випадку глобальну помилку, повідомленя про яку відображується в консолі. У цій ситуації висновок один, потрібно пам’ятати про обробники помилок!
Promise API
resolve
Метод Promise.resolve()
— це статичний метод об’єкта Promise
, який використовується для створення вже виконаного (fulfilled
) проміса з певним значенням. Цей метод може бути корисним, коли потрібно створити Promise
, який завжди повертає певне значення.
Promise.resolve(value);
Де value
— значення, з яким виконається Promise
.
Виклик Promise.resolve(value)
створює успішно виконаний проміс з результатом value
. Це аналогічний, але коротший запис наступного коду:
new Promise((resolve) => resolve(value))
Цей метод використовують, коли потрібно побудувати асинхронний ланцюжок і ми маємо початковий результат.
Promise
.resolve(42)
.then(result => {
console.log(result); // Виведе: 42
});
Ще Promise.resolve() дозволяє нам перетворювати синхронні значення на асинхронні, що полегшує роботу з асинхронним кодом та забезпечує однаковість у обробці асинхронних та синхронних значень у Promise-ланцюжках.
reject
Promise.reject()
— це метод: який використовується для створення вже відхиленого (rejected
) промісу з певною причиною. Цей метод дозволяє явно зазначити, що Promise
завершився з помилкою.
Promise.reject(reason);
Де reason
(необов’язковий) — причина відхилення Promise
. Це може бути рядком, об’єктом помилки Error
або будь-яким іншим значенням, яке треба використати щоб вказати причину.
Це аналогічний, але коротший запис наступного коду:
new Promise((resolve, reject) => reject(error));
Його використання може виглядати наступним чином:
Promise
.reject("Something went wrong")
.catch(error => {
console.error(error); // Виведе: Something went wrong
});
Promise.reject() використовується рідко, але є корисним, коли потрібно явно позначити помилку в асинхронній операції.
all
Метод Promise.all()
— це статичний метод об’єкта Promise
, який використовується для створення нового Promise
, який очікує на виконання всіх переданих до нього Promise
і повертає масив з їх результатами в тому ж порядку, в якому вони були передані.
Метод приймає один аргумент який є ітерабельним об’єктом промісів (зазвичай масив з викликами функцій що повертають проміси). Він повертає Promise
, який буде resolved
тільки у тому випадку, коли кожен Promise
у цьому ітерабельному об’єкті буде завершений успішно. Але! Якщо хоча б один з переданих Promise
завершується з помилкою, то Promise
, що повертається, як результат виклику Promise.all()
— також відхиляється. Promise
–результат буде завершений лише тоді, коли всі Promise
у ітерабельному об’єкті будуть виконані.
Promise.all(iterable);
Де iterable
— ітерабельний об’єкт (наприклад, масив або об’єкт з елементами, що ітеруються), що містить Promise
, які треба виконати.
Розглянемо приклад:
const promiseA = Promise.resolve('Hello');
const promiseB = Promise.resolve('Promise');
const promiseC = Promise.resolve('All');
const promisesCollection = Promise.all([promiseA, promiseB, promiseC]);
promisesCollection
.then(x => x.toString().replaceAll(',', ' '))
.then(console.log);
Виклик Promise.all()
створює проміс promisesCollection
, який буде виконанний, коли проміси promiseA
, promiseB
та promiseC
будуть завершені. Результатом буде масив рядків, які ми обʼєднуємо у один рядок Hello Promise All
.
Якщо хоч один проміс з переданих у Promise.all()
буде відхилено, проміс–результат негайно буде відхилений без очікування завершення всіх інших промісів:
const a = Promise.resolve('Hello');
const b = Promise.reject('Promise all rejected');
const c = Promise.resolve('All');
const promisesCollection = Promise.all([a, b, c]);
promisesCollection
.then(x => x.toString().replaceAll(',', ' '))
.then(console.log)
.catch(console.log);
У цьому прикладі, проміс b відхилений зі значенням Promise all rejected і як наслідок ми отримуємо відхилиний проміс у результаті виконання Promise.all([a, b, c]), який обробляємо у методі catch.
allSettled
Метод Promise.allSettled()
— це статичний метод об’єкта Promise
, який використовується для створення нового Promise
, який очікує на виконання всіх переданих до нього Promise
і повертає масив об’єктів з інформацією про стан кожного переданого Promise
. На відміну від Promise.all()
ми отримаємо результат у будь-якому випадку, навіть якщо жоден проміс не завершиться успішно.
Promise.allSettled(iterable);
Де iterable
— ітерабельний об’єкт (наприклад, масив або об’єкт з елементами, що ітеруються), що містить Promise
, які треба виконати.
Як результат буде повернено массив обʼєктів які мають наступні властивості:
status
— рядок, що вказує на станPromise
. Можливі значення:fulfilled
абоrejected
value
тільки дляfulfilled
) — значення з якимPromise
було успішно виконаноreason
(тільки дляrejected
) — причина, через якуPromise
був відхилений
const promiseA = Promise.resolve('Hello');
const promiseB = Promise.reject('Promise all rejected');
const promiseC = Promise.resolve('allSettled');
const promisesCollection = Promise.allSettled([promiseA, promiseB, promiseC]);
promisesCollection.then(console.log);
/*
[
{
status: 'fulfilled',
value: 'Hello',
},
{
status: 'rejected',
reason: 'Promise all rejected',
},
{
status: 'fulfilled',
value: 'allSettled',
},
];
*/
У цьому прикладі Promise.allSettled() очікує завершення всіх трьох Promise, незалежно від їх успішності. Результатом є масив об’єктів, кожен з яких надає інформацію про стан та значення або причину відхилення Promise. Цей метод дуже корисний, коли вам потрібно виконати кілька асинхронних операцій та отримати інформацію про те, як кожна з них завершилася, незалежно від успіху чи помилки.
race
Метод Promise.race()
— це статичний метод об’єкта Promise
, який використовується для створення нового Promise
, який виконується, як тільки один із переданих Promise
завершиться, чи то успішно, чи з помилкою. Він повертає Promise
, який приймає результат (або помилку) в залежності від того, який результат поверне перший завершений проміс.
Promise.race(iterable);
Де iterable
— ітерабельний об’єкт (наприклад, масив або об’єкт з елементами, що ітеруються), що містить Promise
, які треба виконати.
Розглянемо приклад:
const getDelay = (from, to) => Math.floor(Math.random() * (to - from + 1)) + from;
const promiseA = new Promise(resolve => setTimeout(resolve, getDelay(100, 500), 'First'));
const promiseB = new Promise((resolve, reject) => setTimeout(reject, getDelay(100, 500), 'Second'));
const promiseC = new Promise(resolve => setTimeout(resolve, getDelay(100, 500), 'Third'));
Promise.race([promiseA, promiseB, promiseC])
.then(x => console.log('Fulfilled: ', x))
.catch(x => console.log('Rejected: ', x));
У цьому прикладі Promise.race() створює новий Promise, який виконується, як тільки один із промісів завершиться. Promise.race() корисний у ситуаціях, коли вам потрібно виконати кілька асинхронних операцій і отримати результат виконаної з них в незалежності від її результату.
any
Метод Promise.any()
— це статичний метод об’єкта Promise
, який використовується для створення нового Promise
, який виконується, як тільки хоча б один із переданих Promise
завершиться успішно. Він повертає Promise
, який приймає перший Promise
, що успішно завершився, і його результат.
Promise.any(iterable);
Де iterable
— ітерабельний об’єкт (наприклад, масив або об’єкт з елементами, що ітеруються), що містить Promise
, які треба виконати.
Розглянемо приклад:
const getDelay = (from, to) => Math.floor(Math.random() * (to - from + 1)) + from;
const promiseA = new Promise(resolve => setTimeout(resolve, getDelay(100, 500), 'First'));
const promiseB = new Promise((resolve, reject) => setTimeout(reject, getDelay(100, 500), 'Second'));
const promiseC = new Promise((resolve, reject) => setTimeout(reject, getDelay(100, 500), 'Third'));
Promise.any([promiseA, promiseB, promiseC])
.then(x => console.log('Fulfilled: ', x))
.catch(x => console.log('Rejected: ', x));
У цьому прикладі ми завжди будемо бачити результат виконання promiseA тому що він є єдиним, який завершується успішно. Але, що буде якщо всі проміси завершаться з помилкою? У такому випадку ми отрамаємо помилку Rejected: AggregateError: All promises were rejected. Давайте подивимося:
const getDelay = (from, to) => Math.floor(Math.random() * (to - from + 1)) + from;
const promiseA = new Promise((resolve, reject) => setTimeout(reject, getDelay(100, 500), 'First'));
const promiseB = new Promise((resolve, reject) => setTimeout(reject, getDelay(100, 500), 'Second'));
const promiseC = new Promise((resolve, reject) => setTimeout(reject, getDelay(100, 500), 'Third'));
Promise.any([promiseA, promiseB, promiseC])
.then(x => console.log('Fulfilled: ', x))
.catch(x => console.log('Rejected: ', x));
// Rejected: AggregateError: All promises were rejected
Async/await
Синтаксис async/await
призначений для спрощення асинхронного програмування при роботі з Promise
. Цей механізм робить асинхронний код більш читаним та зрозумілим, особливо при роботі з великою кількістю асинхронних операцій та ланцюжками викликів.
Ключове слово async
Ключове слово async
використовується для оголошення асинхронних функцій. Ці функції завжди повертають Promise
та дозволяють зручніше працювати з асинхронними операціями. Це виглядає наступним чином:
async function hello(){
return 'Hello'
}
Тепер виклик функції повертає Promise. Це одна з особливостей асинхронних функцій, вони повертають значення, які гарантовано перетворюються на Promise. Щоб отримати значення завершеного Promise, можемо використовувати метод then():
async function hello(){
return 'Hello'
}
hello().then(console.log)
Ключове слово await
Ключове слово await
використовується всередині асинхронних функцій, оголошених з ключовим словом async
, і дозволяє призупиняти виконання функції доти, доки асинхронна операція не завершиться.
Тобто await
може бути додана перед будь-якою функцією, що базується на Promise
, щоб змусити її чекати завершення, а потім повернути результат. Після цього виконується наступний блок коду.
Основні аспекти ключового слова await
:
async function fetchData() {
const response = await fetch('https://swapi.dev/api/people/1/');
const data = await response.json();
return data;
}
У цьому прикладі await використовується для очікування завершення запиту fetch і аналізу JSON-відповіді.
Обробка помилок
Обробка помилок, що виникають в асинхронних операціях при використанні async/await
, можна здійснити за допомогою вже знайомого нам try...catch
. Аналогічно синхронному коду ви можете використовувати конструкцію try...catch
для обробки помилок в асинхронних функціях з await
. Це дозволяє ловити помилки та обробляти їх у блоці catch
.
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Request failed');
}
const data = await response.json();
return data;
} catch (error) {
console.error('An error occurred:', error);
throw error;
}
}
Fetch API
Fetch API – це сучасний спосіб виконання мережевих запитів у браузері. Він надає можливість взаємодіяти з сервером за допомогою HTTP-запитів, таких як GET, POST, PUT, DELETE тощо. Fetch API використовує проміси для обробки асинхронного коду, що робить його більш читабельним та потужним.
В основі використання Fetch API лежить функція fetch()
, яка приймає URL запиту і може приймати об’єкт параметрів для налаштування запиту, таких як метод, заголовки, тіло запиту тощо. Після виклику fetch()
, ви отримаєте об’єкт Response
, з яким ви можете працювати.
Ось приклад використання Fetch API з різними HTTP методами:
// GET запит
fetch('https://api.example.com/users/1')
.then(response => response.json())
.then(data => {
console.log('GET Response:', data);
})
.catch(error => {
console.error('Error:', error);
});
// POST запит
const newUser = {
name: 'Alice',
email: 'alice@example.com'
};
fetch('https://api.example.com/users/1', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newUser)
})
.then(response => response.json())
.then(data => {
console.log('POST Response:', data);
})
.catch(error => {
console.error('Error:', error);
});
// PUT запит
const updatedUser = {
name: 'Alice Smith'
};
fetch('https://api.example.com/users/1', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedUser)
})
.then(response => response.json())
.then(data => {
console.log('PUT Response:', data);
})
.catch(error => {
console.error('Error:', error);
});
// DELETE запит
fetch('https://api.example.com/users/1', {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
console.log('User deleted successfully');
} else {
console.error('Error:', response.statusText);
}
})
.catch(error => {
console.error('Error:', error);
});
Це приклади базового використання Fetch API для різних HTTP методів. Пам’ятайте, що Fetch API повертає проміси, тому ми використовуємо методи .then()
та .catch()
для обробки результатів асинхронного запиту.
А ось як використовувати fetch API з async/await
async function fetchData(url, options) {
try {
const response = await fetch(url, options);
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
async function getUserData() {
try {
const users = await fetchData('https://api.example.com/users');
console.log('GET Response:', users);
} catch (error) {
console.error('Error:', error);
}
}
async function createUser(newUser) {
try {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newUser)
};
const createdUser = await fetchData('https://api.example.com/users', options);
console.log('POST Response:', createdUser);
} catch (error) {
console.error('Error:', error);
}
}
async function updateUser(updatedUser) {
try {
const options = {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedUser)
};
const updatedUserData = await fetchData('https://api.example.com/users/1', options);
console.log('PUT Response:', updatedUserData);
} catch (error) {
console.error('Error:', error);
}
}
async function deleteUser() {
try {
const options = {
method: 'DELETE'
};
const response = await fetch('https://api.example.com/users/1', options);
if (response.ok) {
console.log('User deleted successfully');
} else {
console.error('Error:', response.statusText);
}
} catch (error) {
console.error('Error:', error);
}
}
// Виклик функцій
getUserData();
createUser({
name: 'Alice',
email: 'alice@example.com'
});
updateUser({
name: 'Alice Smith'
});
deleteUser();
Підсумок
Асинхронний код
Асинхронний код в JavaScript дозволяє виконувати операції, не чекаючи завершення попередніх. Це спосіб оптимізації виконання задач, які можуть займати тривалий час, таких як мережеві запити або операції вводу-виводу.
Функції зворотного виклику
Функції зворотного виклику (callback functions) – це функції, які передаються як аргументи іншим функціям і викликаються у визначений момент, часто після завершення певної операції.
Таймери
setTimeout
і setInterval
використовуються для виконання коду через певний проміжок часу або регулярно кожний певний інтервал. clearTimeout
та clearInterval
використовуються для зупинки виконання коду, що був запланований для виконання.
Event Loop
Event Loop – це механізм, що дозволяє JavaScript обробляти асинхронний код та події. Він використовує Stack для виконання функцій та Queue для обробки подій.
Promise
Promise
– це об’єкт, який представляє результат асинхронної операції. Він може бути в стані pending, resolved або rejected.
Promise API
resolve
та reject
використовуються для встановлення стану об’єкта Promise. all
, allSettled
, race
та any
дозволяють обробляти багато обіцянок одночасно.
Async/await
async
та await
використовуються для написання асинхронного коду у зрозумілій, синхронній формі. Це спрощує читання та розуміння коду.
Fetch API
Fetch API – це інтерфейс JavaScript для виконання мережевих запитів. Він використовується для отримання ресурсів з мережі та обміну даними з сервером.