Що таке async/await у JavaScript
async/await - це синтаксичний цукор над Promise, доданий у ES2017, який дозволяє писати асинхронний JavaScript-код рядок за рядком, наче він синхронний.
Теорія
TL;DR
asyncробить функцію асинхронною і автоматично загортає повернуте значення в Promiseawaitпризупиняє тільки туasyncфункцію, де стоїть, не блокуючи головний потік- Аналогія: замовляєш каву в кафе - відходиш убік (await зупиняє функцію), черга рухається далі (event loop виконує інший код), забираєш коли готово (Promise виконується)
- Використовуй async/await для 2+ послідовних асинхронних операцій; для одиночних - звичайний Promise
- Обробка помилок:
try/catchзамість ланцюжків.catch()
Короткий приклад
// Promise-ланцюжок - читається зсередини назовні
fetch('/api/user')
.then(res => res.json())
.then(user => console.log(user.name))
.catch(err => console.error(err));
// async/await - читається зверху вниз
async function getUser() {
try {
const res = await fetch('/api/user');
const user = await res.json();
console.log(user.name); // "Alice"
} catch (err) {
console.error(err);
}
}Обидва варіанти роблять одне й те саме. Але async/await читається в тому порядку, в якому все відбувається.
Головна різниця
await перетворює Promise у значення, яке можна одразу присвоїти змінній, зупиняючи async функцію на цьому рядку до завершення Promise. Event loop при цьому продовжує працювати: інший код, таймери та обробники подій нікуди не зникають. Коли Promise виконується, продовження функції стає в чергу мікрозавдань (microtask queue), і виконання поновлюється з того місця, де зупинилось.
Коли використовувати
- Послідовні API-запити (отримати юзера, потім його пости): async/await читається природно
- Обробка помилок у кількох кроках: один
try/catchпокриває всіawaitв блоці - Паралельні операції:
Promise.all()всерединіasyncфункції - Один простий запит: звичайний Promise з
.then()цілком підходить - Легасі-код або бібліотеки без підтримки Promise: колбеки або Promises безпосередньо
Як це працює всередині
V8 компілює async функції у стейт-машини на основі генераторів. Кожен await передає управління event loop через Generator.prototype.next(). Коли очікуваний Promise виконується, продовження функції стає в чергу мікрозавдань, яка обробляється до макрозавдань типу setTimeout. Тобто код після await завжди виконується раніше ніж колбеки від таймерів.
Типові помилки
Забули await перед Promise
async function bad() {
const data = fetch('/api/user'); // Пропущено await!
console.log(data); // Promise { <pending> }
}Функція продовжує виконання одразу. data містить об'єкт Promise, а не відповідь сервера. Рішення: додати await перед fetch.
throw без обробки поверненого Promise
async function risky() {
throw new Error('boom'); // Стає відхиленим Promise
}
risky(); // Unhandled rejection - падає Node.jsПомилка не піднімається синхронно. Вона відхиляє Promise, який повертає risky(). Потрібно або додати .catch() на виклик, або викликати в async функції з try/catch.
await поза async функцією
function wrong() {
const data = await fetch('/api'); // SyntaxError
}await працює тільки всередині async функцій (або на верхньому рівні ES-модулів). Рішення: позначити функцію як async.
Думати, що await блокує всю програму
async function a() { await delay(1000); console.log('A'); }
async function b() { console.log('B'); }
a();
b();
// Виведе: "B", потім "A"await зупиняє тільки свою async функцію. Всі інші продовжують виконуватись. Це найпоширеніша помилка на співбесідах, коли питають про порядок виконання.
Послідовні await там, де можна паралельно
// Повільно: кожен запит чекає завершення попереднього
const user = await fetchUser(id);
const posts = await fetchPosts(id);
// Швидко: обидва стартують одночасно
const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);Якщо дві операції не залежать одна від одної, немає причини запускати їх послідовно. Це найчастіша помилка, яку я бачу на code review, і виправляється в один рядок.
Де зустрічається в реальному коді
- React/Next.js:
const data = await fetchUser(id)у Server Components абоgetServerSideProps - Express:
const user = await db.query('SELECT * FROM users WHERE id = ?', [id])в обробниках маршрутів - Node.js:
const file = await fs.readFile('data.txt', 'utf8')через модульfs/promises - Axios:
const res = await axios.get('/api/posts')у Vue або Nuxt - Puppeteer:
await page.goto(url)в скриптах автоматизації браузера
Питання на співбесіді
Q: Що повертає async функція, якщо написати return 42?
A: Завжди Promise. Рушій загортає значення через Promise.resolve(42). Саме число 42 отримаєш тільки після await або через .then().
Q: Чи можна await не Promise-значення, наприклад число?
A: Так. await 42 одразу повертає 42. Технічно рушій загортає це в Promise.resolve(). Працює, але рідко має сенс поза загальними утилітами.
Q: Як await обробляє відхилення Promise?
A: Кидає причину відхилення всередину async функції як звичайну помилку. Навколишній try/catch її перехопить.
Q: Як скасувати операцію, яку вже очікуємо через await?
A: Самі Promise не можна скасувати, але fetch приймає сигнал від AbortController. Передай { signal: ac.signal } у запит і виклич ac.abort() коли потрібно.
Q: Яка різниця між мікрозавданнями і макрозавданнями у контексті await? (рівень senior)
A: Продовження після await стає в чергу мікрозавдань (microtask queue). Мікрозавдання виконуються після поточного синхронного коду, але до макрозавдань типу setTimeout. Тобто код після await somePromise виконається раніше ніж будь-який setTimeout(fn, 0). Саме на цьому порядку базується планувальник React Concurrent mode.
Приклади
Базовий: що повертає async функція
async function add(a, b) {
return a + b; // Автоматично загортається в Promise.resolve()
}
// Щоб отримати значення - потрібен .then() або await
add(2, 3).then(result => console.log(result)); // 5async загортає будь-яке повернуте значення в Promise. Навіть просту арифметику. Число отримаєш тільки через Promise.
Середній: обробник входу в Express
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email }); // Запит до БД
if (!user || !await bcrypt.compare(password, user.hash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id }, process.env.SECRET);
res.json({ token });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// Успіх: { token: "eyJ..." }
// Помилка автентифікації: 401 { error: "Invalid credentials" }Дві послідовні асинхронні операції - запит до БД і перевірка пароля - кожен рядок чекає попереднього. Один try/catch покриває обидві. Саме так виглядає async/await у реальному Node.js-коді.
Просунутий: гонки стану у паралельних операціях
// Виглядає безпечно, але не є:
async function tricky() {
let x = 0;
const p1 = (async () => { await null; x = 1; })();
const p2 = (async () => { await null; x = 2; })();
await Promise.all([p1, p2]);
console.log(x); // 1 або 2 - невизначено
}
// Правильний патерн для паралельних запитів:
async function loadDashboard(userId) {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId)
]);
return { user, posts, notifications };
}Promise.all() запускає всі три паралельно і повертає результати в тому ж порядку, в якому вони передані - незалежно від того, який Promise виконався першим. Функція tricky() вище - реальна гонка стану (race condition): обидва IIFE записують у x після одного такту мікрозавдань, і порядок непередбачуваний. Уникай спільного змінного стану в паралельних async операціях.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.