Промисы и async/await в JavaScript: практическое руководство
Практика3 минРедакция Innovialabs
Коротко
Промис — это объект с будущим результатом, а async/await — удобный синтаксис над ним; независимые запросы запускайте параллельно через Promise.all, а ошибки ловите через try/catch.
Коротко о главном
Промис — объект, который представляет результат асинхронной операции: он либо выполнится (fulfilled), либо отклонится с ошибкой (rejected). async/await — синтаксис поверх промисов, который позволяет писать асинхронный код так, будто он синхронный.
Три правила, которые закрывают большинство задач:
- ошибки ловите через
try/catchвокругawait; - независимые операции запускайте параллельно через
Promise.all; - не забывайте
await— иначе получите промис вместо данных.
От колбэков к промисам
Раньше асинхронность строилась на колбэках, и вложенность быстро росла:
getUser(id, (err, user) => {
if (err) return handle(err);
getOrders(user.id, (err, orders) => {
if (err) return handle(err);
render(orders);
});
});
С промисами это превращается в плоскую цепочку с одной точкой обработки ошибок:
getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => render(orders))
.catch(handle);
async/await: тот же код, только читаемее
async function showOrders(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
render(orders);
} catch (err) {
handle(err);
} finally {
hideLoader();
}
}
Важно помнить: async-функция всегда возвращает промис. Даже return 5 внутри неё вернёт Promise<5>.
Если ответ сервера с ошибкой (например, статус 404), fetch не отклоняет промис — проверяйте response.ok сами:
const res = await fetch('/api/items');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const items = await res.json();
Параллельно или последовательно
Частая ошибка — ждать независимые запросы по очереди:
// последовательно: второй запрос стартует только после первого
const user = await getUser(id);
const news = await getNews();
Если запросы не зависят друг от друга, запустите их одновременно:
const [user, news] = await Promise.all([getUser(id), getNews()]);
Последовательность нужна, только когда следующий шаг использует результат предыдущего.
Promise.all, allSettled, race, any
| Метод | Когда завершается | Когда использовать |
|---|---|---|
Promise.all | все успешны или первая ошибка | все результаты обязательны |
Promise.allSettled | когда все завершились | нужны и успехи, и ошибки |
Promise.race | первый завершившийся, неважно как | таймауты |
Promise.any | первый успешный | несколько зеркал одного ресурса |
Пример таймаута через race:
const timeout = (ms) =>
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms));
const data = await Promise.race([fetchData(), timeout(5000)]);
Для отмены самого fetch используйте AbortController — race лишь перестаёт ждать, но запрос продолжает выполняться.
Частые ошибки
awaitвнутриforEach.forEachне ждёт промисы. Используйтеfor...ofдля последовательной обработки илиPromise.allсmapдля параллельной.- Забытый
await. В переменной окажется промис, а ошибка уйдёт в необработанные. - Пустой
catch. Проглоченная ошибка — худший вид бага. Хотя бы логируйте её. - Слишком много параллельных запросов.
Promise.allпо тысяче элементов может перегрузить API. Обрабатывайте пачками. - Смешивание стилей. В одной функции лучше выбрать либо
then, либоawait.
// неправильно
items.forEach(async (item) => await save(item));
// параллельно
await Promise.all(items.map((item) => save(item)));
// последовательно
for (const item of items) {
await save(item);
}