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 не был awaited }); // Исправлено: 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);
}