JavaScript · Синтаксис · Проміжний

Проміси і async/await

10 завдань

Обробка асинхронних операцій за допомогою ланцюжків Promise, async-функцій та await.

Що таке Promise: стани, ланцюжок then та черга мікрозадач

#
Promise представляє значення, яке може ще не бути доступним. Він перебуває в одному з трьох станів: **pending** (початковий), **fulfilled** (виконаний зі значенням) або **rejected** (відхилений з причиною). ```javascript const p = new Promise((resolve, reject) => { // виконавець запускається синхронно if (Math.random() > 0.5) { resolve('успіх'); // перехід до fulfilled } else { reject(new Error('невдача')); // перехід до rejected } }); p.then(value => console.log(value)) // 'успіх' .catch(err => console.log(err.message)); // 'невдача' ``` **Черга мікрозадач** Зворотні виклики Promise (`.then`, `.catch`, `.finally`) не запускаються одразу — вони потрапляють до *черги мікрозадач* і виконуються після завершення поточного синхронного коду, але перед наступною макрозадачею (setTimeout, setInterval, I/O): ```javascript console.log('1'); Promise.resolve().then(() => console.log('2')); console.log('3'); // Вивід: 1 3 2 // '2' виконується після '3', оскільки зворотні виклики .then є мікрозадачами ``` **Ланцюжок then** `.then()` повертає новий Promise. Їх можна ланцюжком, щоб виконувати кроки послідовно: ```javascript fetchUser(id) .then(user => fetchPosts(user.id)) // ОБОВ'ЯЗКОВО повертати наступний Promise .then(posts => render(posts)) .catch(err => showError(err)); ``` **Найпоширеніша помилка в ланцюжках: відсутній return** Якщо забути `return` всередині `.then`, ланцюжок не чекає на внутрішній Promise — він виконується з `undefined`, і наступний `.then` спрацьовує одразу з `undefined`: ```javascript // Помилка: відсутній return fetchUser(id) .then(user => { fetchPosts(user.id); // забули 'return'! }) .then(posts => { console.log(posts); // undefined — fetchPosts не очікувався }); // Виправлено: fetchUser(id) .then(user => fetchPosts(user.id)) // стрілка без фігурних дужок неявно повертає .then(posts => console.log(posts)); ``` **Створення вже виконаних/відхилених Promise** ```javascript Promise.resolve(42).then(v => console.log(v)); // 42 Promise.reject(new Error('ой')).catch(e => console.log(e.message)); // 'ой' ``` Зручні для огортання синхронних значень у Promise-сумісний інтерфейс.

async/await: обробка помилок, необроблене відхилення, послідовно vs паралельно

#
Функції `async` завжди повертають Promise. `await` призупиняє виконання всередині async-функції до виконання Promise — навколишній код не блокується. ```javascript async function getUser(id) { const user = await fetchUser(id); // чекає fetchUser const posts = await fetchPosts(user.id); // потім чекає fetchPosts return { user, posts }; } // async-функція повертає Promise getUser(1).then(data => console.log(data)); ``` **Обробка помилок з async/await** Використовуй `try/catch` навколо викликів `await` — це перехоплює і синхронні throw, і відхилені Promise: ```javascript async function loadData(id) { try { const data = await fetchData(id); return process(data); } catch (err) { console.error('Помилка:', err.message); return null; } finally { hideLoadingSpinner(); // завжди виконується } } ``` **Необроблене відхилення** Якщо відхилений Promise не має `.catch` і немає навколишнього `try/catch`, він стає необробленим відхиленням. У Node.js це завершує процес; у браузерах спрацьовує `window.unhandledrejection`: ```javascript // Небезпечно: немає обробки помилок async function bad() { const data = await fetchData(); // якщо відхилено, помилка зникає мовчки return data; } // Краще: завжди обробляй bad().catch(err => console.error(err)); ``` **Послідовне vs паралельне виконання** Два `await` підряд виконуються послідовно (другий чекає першого). Щоб виконувати паралельно, запусти обидва Promise *перед* очікуванням будь-якого: ```javascript // Послідовно: загальний час = час(A) + час(B) async function sequential() { const a = await fetchA(); const b = await fetchB(); // починається лише після завершення fetchA return [a, b]; } // Паралельно: загальний час = max(час(A), час(B)) async function parallel() { const [a, b] = await Promise.all([fetchA(), fetchB()]); return [a, b]; } ``` **Змішування async/await з .then** Їх можна змішувати — `await` працює з будь-яким thenable, а результат async-функції можна ланцюжком через `.then`: ```javascript const result = await someAsyncFn().then(v => transform(v)); ``` Але змішування стилів в одному ланцюжку знижує читабельність. Перевагу надавай одному стилю.

Комбінатори Promise: all, allSettled, race, any

#
**Promise.all** — всі мають успішно завершитися, інакше негайна відмова: ```javascript const [user, settings, feed] = await Promise.all([ fetchUser(id), fetchSettings(id), fetchFeed(id), ]); // Якщо будь-який відхилено, все негайно відхиляється. // Інші продовжують виконуватися, але їхні результати ігноруються. ``` **Promise.allSettled** — чекати всіх незалежно від успіху/невдачі: ```javascript const results = await Promise.allSettled([ fetchUser(id), fetchSettings(id), ]); results.forEach(r => { if (r.status === 'fulfilled') console.log(r.value); else console.error(r.reason); }); // Використовуй, коли потрібні всі результати, навіть часткові. ``` **Promise.race** — виконується, як тільки перший завершується: ```javascript const result = await Promise.race([ fetchData(), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000)), ]); // Поширене використання: додати тайм-аут до будь-якої async-операції ``` **Promise.any** (ES2021) — виконується за першим *успішним* результатом, ігнорує відхилення: ```javascript const data = await Promise.any([ fetchFromCDN1(), fetchFromCDN2(), fetchFromCDN3(), ]); // Використовуй для резервних запитів: перший CDN, що відповість, перемагає. // Відхиляється з AggregateError лише якщо ВСІ відхилені. ``` **Порівняння комбінаторів** ``` Promise.all - всі успішні або негайна відмова. Результат: масив значень Promise.allSettled - чекає всіх. Результат: масив {status, value/reason} Promise.race - перший виконаний перемагає (успіх або відмова) Promise.any - перший успішний перемагає; відмова лише якщо всі відхилені ``` **Послідовна async-ітерація з for...of** Коли потрібно обробляти елементи по одному (наприклад, обмеження API), використовуй `for...of` — `map` з `Promise.all` запустив би всі запити паралельно: ```javascript async function processSequentially(items) { const results = []; for (const item of items) { const result = await processItem(item); // чекає кожного results.push(result); } return results; } ```

Async-патерни: повтор, обмеження паралельності, тайм-аут, черга, кеш

#
Реальні async-патерни, що часто зустрічаються. **Повтор з експоненційним відступом** ```javascript async function retry(fn, maxAttempts = 3, delay = 500) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (err) { if (attempt === maxAttempts) throw err; await new Promise(r => setTimeout(r, delay * attempt)); } } } const data = await retry(() => fetchData(id), 3, 1000); // Намагається до 3 разів, чекаючи 1с, 2с, 3с між спробами ``` **Паралельне виконання з обмеженням паралельності** ```javascript async function mapWithLimit(items, fn, limit = 3) { const results = []; for (let i = 0; i < items.length; i += limit) { const batch = items.slice(i, i + limit); const batchResults = await Promise.all(batch.map(fn)); results.push(...batchResults); } return results; } // Обробляти 100 URL по 3 одночасно const pages = await mapWithLimit(urls, fetchPage, 3); ``` **Обгортка з тайм-аутом** ```javascript function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`Тайм-аут ${ms}мс`)), ms) ); return Promise.race([promise, timeout]); } const data = await withTimeout(fetchData(), 5000); ``` **Async-черга (по одному)** ```javascript function makeQueue() { let running = Promise.resolve(); return (fn) => { running = running.then(fn); return running; }; } const queue = makeQueue(); queue(() => save(a)); // виконуються послідовно queue(() => save(b)); queue(() => save(c)); ``` **Кешування async-результатів** ```javascript function asyncCache(fn) { const cache = new Map(); return async (key) => { if (!cache.has(key)) { cache.set(key, fn(key)); // зберігати сам Promise, а не результат } return cache.get(key); }; } const cachedFetch = asyncCache(fetchUser); const [u1, u2] = await Promise.all([cachedFetch(1), cachedFetch(1)]); // fetchUser(1) викликається лише один раз, навіть при одночасних запитах ```
01

Створити Promise

#

Напишіть функцію delay(ms), яка повертає Promise, що виконується після заданої кількості мілісекунд.

function delay(ms) {

}

delay(1000).then(() => console.log("Done after 1 second"));
Рішення
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

delay(1000).then(() => console.log("Done after 1 second"));
02

Promise зі значенням

#

Напишіть функцію fetchUser(id), яка повертає Promise, що виконується через 500 мс з об'єктом-заглушкою { id, name }. Якщо id дорівнює 0, відхилити з повідомленням 'Invalid ID'.

function fetchUser(id) {

}

fetchUser(1).then(user => console.log(user));
fetchUser(0).catch(err => console.log(err));
Рішення
function fetchUser(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (id === 0) {
                reject('Invalid ID');
            } else {
                resolve({ id, name: `User ${id}` });
            }
        }, 500);
    });
}

fetchUser(1).then(user => console.log(user));
fetchUser(0).catch(err => console.log(err));
03

async/await базовий

#

Перепишіть наступний ланцюжок Promise за допомогою async/await. Функція має отримати користувача, потім вивести його ім'я. Обробляйте помилки через try/catch. Оригінал: fetchUser(id).then(u => console.log(u.name)).catch(e => console.log('Error:', e))

// Припускається, що fetchUser(id) повертає Promise (з попереднього завдання)
async function showUserName(id) {

}

showUserName(1);
showUserName(0);
Рішення
async function showUserName(id) {
    try {
        const user = await fetchUser(id);
        console.log(user.name);
    } catch (e) {
        console.log('Error:', e);
    }
}

showUserName(1);
showUserName(0);
04

Promise.all

#

Напишіть async-функцію fetchAll(ids), яка приймає масив ID користувачів, паралельно отримує всіх користувачів через Promise.all і повертає масив об'єктів користувачів.

// Припускається, що fetchUser(id) доступна
async function fetchAll(ids) {

}

fetchAll([1, 2, 3]).then(users => console.log(users));
Рішення
async function fetchAll(ids) {
    return Promise.all(ids.map(id => fetchUser(id)));
}

fetchAll([1, 2, 3]).then(users => console.log(users));
05

Послідовні async-операції

#

Напишіть async-функцію processUsers(ids), яка отримує користувачів по одному (послідовно, не паралельно) і повертає масив їхніх імен.

async function processUsers(ids) {

}

processUsers([1, 2, 3]).then(names => console.log(names));
Рішення
async function processUsers(ids) {
    const names = [];
    for (const id of ids) {
        const user = await fetchUser(id);
        names.push(user.name);
    }
    return names;
}

processUsers([1, 2, 3]).then(names => console.log(names));
06

Promise.allSettled

#

Напишіть async-функцію fetchWithResults(ids), яка паралельно намагається отримати всіх користувачів і повертає масив об'єктів { id, user } для успішних запитів і { id, error } для невдалих. Використайте Promise.allSettled.

async function fetchWithResults(ids) {

}

fetchWithResults([1, 0, 2]).then(results => console.log(results));
Рішення
async function fetchWithResults(ids) {
    const results = await Promise.allSettled(ids.map(id => fetchUser(id)));
    return results.map((result, i) => {
        if (result.status === 'fulfilled') {
            return { id: ids[i], user: result.value };
        } else {
            return { id: ids[i], error: result.reason };
        }
    });
}

fetchWithResults([1, 0, 2]).then(results => console.log(results));
07

Async з таймаутом

#

Напишіть async-функцію withTimeout(promise, ms), яка змагає заданий проміс із таймаутом. Якщо проміс не виконується протягом ms мілісекунд, відхилити з 'Timeout'.

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function withTimeout(promise, ms) {

}

// Тест: повільна операція, що займає 2 секунди
const slow = delay(2000).then(() => 'done');
withTimeout(slow, 1000)
    .then(r => console.log(r))
    .catch(e => console.log(e));
Рішення
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) =>
        setTimeout(() => reject('Timeout'), ms)
    );
    return Promise.race([promise, timeout]);
}

const slow = delay(2000).then(() => 'done');
withTimeout(slow, 1000)
    .then(r => console.log(r))
    .catch(e => console.log(e));
08

Повтор при невдачі

#

Напишіть async-функцію retry(fn, attempts), яка викликає fn() (що повертає Promise) до `attempts` разів. Якщо fn успішна, повернути результат. Якщо всі спроби зазнали невдачі, викинути останню помилку.

async function retry(fn, attempts) {

}

let callCount = 0;
const flaky = () => new Promise((resolve, reject) => {
    callCount++;
    if (callCount < 3) reject(new Error(`Attempt ${callCount} failed`));
    else resolve('Success!');
});

retry(flaky, 5).then(console.log).catch(console.error);
Рішення
async function retry(fn, attempts) {
    let lastError;
    for (let i = 0; i < attempts; i++) {
        try {
            return await fn();
        } catch (e) {
            lastError = e;
        }
    }
    throw lastError;
}

let callCount = 0;
const flaky = () => new Promise((resolve, reject) => {
    callCount++;
    if (callCount < 3) reject(new Error(`Attempt ${callCount} failed`));
    else resolve('Success!');
});

retry(flaky, 5).then(console.log).catch(console.error);
09

Promise chain vs async/await

#

Перепишіть наступний ланцюжок Promise як async-функцію. Оригінал отримує користувача, потім його пости, потім повертає заголовки всіх постів. Оригінал (getUser(id) і getUserPosts(userId) повертають Promise): getUser(id).then(u => getUserPosts(u.id)).then(posts => posts.map(p => p.title))

// Припускається, що getUser(id) і getUserPosts(userId) повертають Promise
async function getUserPostTitles(id) {

}

getUserPostTitles(1).then(titles => console.log(titles));
Рішення
async function getUserPostTitles(id) {
    const user = await getUser(id);
    const posts = await getUserPosts(user.id);
    return posts.map(p => p.title);
}

getUserPostTitles(1).then(titles => console.log(titles));
10

Поширення помилок у async

#

Напишіть async-функцію pipeline(id), яка: 1) отримує користувача, 2) якщо ім'я починається з 'A', кидає помилку 'Name starts with A', 3) інакше виводить ім'я. Оберніть виклик pipeline() у try/catch, який логує будь-яку помилку.

async function pipeline(id) {

}

try {
    await pipeline(1);
} catch (e) {
    console.log('Caught:', e.message || e);
}
Рішення
async function pipeline(id) {
    const user = await fetchUser(id);
    if (user.name.startsWith('A')) {
        throw new Error('Name starts with A');
    }
    console.log(user.name);
}

try {
    await pipeline(1);
} catch (e) {
    console.log('Caught:', e.message || e);
}