Skip to main content

Що таке async/await у JavaScript

async/await - це синтаксичний цукор над Promise, доданий у ES2017, який дозволяє писати асинхронний JavaScript-код рядок за рядком, наче він синхронний.

Теорія

TL;DR

  • async робить функцію асинхронною і автоматично загортає повернуте значення в Promise
  • await призупиняє тільки ту async функцію, де стоїть, не блокуючи головний потік
  • Аналогія: замовляєш каву в кафе - відходиш убік (await зупиняє функцію), черга рухається далі (event loop виконує інший код), забираєш коли готово (Promise виконується)
  • Використовуй async/await для 2+ послідовних асинхронних операцій; для одиночних - звичайний Promise
  • Обробка помилок: try/catch замість ланцюжків .catch()

Короткий приклад

javascript
// 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

javascript
async function bad() { const data = fetch('/api/user'); // Пропущено await! console.log(data); // Promise { <pending> } }

Функція продовжує виконання одразу. data містить об'єкт Promise, а не відповідь сервера. Рішення: додати await перед fetch.

throw без обробки поверненого Promise

javascript
async function risky() { throw new Error('boom'); // Стає відхиленим Promise } risky(); // Unhandled rejection - падає Node.js

Помилка не піднімається синхронно. Вона відхиляє Promise, який повертає risky(). Потрібно або додати .catch() на виклик, або викликати в async функції з try/catch.

await поза async функцією

javascript
function wrong() { const data = await fetch('/api'); // SyntaxError }

await працює тільки всередині async функцій (або на верхньому рівні ES-модулів). Рішення: позначити функцію як async.

Думати, що await блокує всю програму

javascript
async function a() { await delay(1000); console.log('A'); } async function b() { console.log('B'); } a(); b(); // Виведе: "B", потім "A"

await зупиняє тільки свою async функцію. Всі інші продовжують виконуватись. Це найпоширеніша помилка на співбесідах, коли питають про порядок виконання.

Послідовні await там, де можна паралельно

javascript
// Повільно: кожен запит чекає завершення попереднього 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 функція

javascript
async function add(a, b) { return a + b; // Автоматично загортається в Promise.resolve() } // Щоб отримати значення - потрібен .then() або await add(2, 3).then(result => console.log(result)); // 5

async загортає будь-яке повернуте значення в Promise. Навіть просту арифметику. Число отримаєш тільки через Promise.

Середній: обробник входу в Express

javascript
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-коді.

Просунутий: гонки стану у паралельних операціях

javascript
// Виглядає безпечно, але не є: 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 операціях.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?