Асинхронный код в JavaScript поначалу кажется сложным: колбэки нарастают лесенкой, цепочки промисов запутываются, ошибки теряются где-то внутри. В этой статье разберём промисы и async/await от основ до продвинутых приёмов — с рабочими примерами, которые можно сразу применять в проектах.
Что такое промис и зачем он нужен
Промис (Promise) — это объект, который представляет результат асинхронной операции: он либо выполнится успешно (fulfilled), либо завершится ошибкой (rejected), либо ещё ждёт результата (pending).
Без промисов асинхронный код выглядел так — «callback hell»:
// Так делать не надо — callback hell
getUser(id, function(user) {
getPosts(user.id, function(posts) {
getComments(posts[0].id, function(comments) {
// ещё один уровень...
}, function(err) { console.error(err); });
}, function(err) { console.error(err); });
}, function(err) { console.error(err); });
Промисы решают эту проблему — асинхронные операции выстраиваются в читаемую цепочку:
// Создание промиса вручную
const delay = (ms) => new Promise((resolve, reject) => {
if (ms < 0) {
reject(new Error('Задержка не может быть отрицательной'));
return;
}
setTimeout(() => resolve(`Прошло ${ms}мс`), ms);
});
// Использование
delay(1000)
.then(result => console.log(result)) // "Прошло 1000мс"
.catch(err => console.error(err.message));
Конструктор new Promise(executor) принимает функцию с двумя параметрами: resolve (вызывается при успехе) и reject (при ошибке). После вызова одного из них состояние промиса фиксируется навсегда.
Цепочки промисов: .then(), .catch(), .finally()
Каждый .then() возвращает новый промис, что позволяет строить цепочки. Если внутри .then() вернуть значение — следующий .then() получит его. Если вернуть промис — цепочка дождётся его выполнения.
// Цепочка промисов для работы с API
fetch('https://api.example.com/users/1')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ошибка: ${response.status}`);
}
return response.json(); // возвращаем новый промис
})
.then(user => {
console.log('Пользователь:', user.name);
return fetch(`https://api.example.com/posts?userId=${user.id}`);
})
.then(response => response.json())
.then(posts => {
console.log(`Найдено постов: ${posts.length}`);
})
.catch(err => {
// Ловит ошибки из ЛЮБОГО шага цепочки
console.error('Что-то пошло не так:', err.message);
})
.finally(() => {
// Выполняется всегда — и при успехе, и при ошибке
console.log('Запрос завершён');
});
Важное правило: .catch() в конце цепочки перехватывает ошибки из всех предшествующих шагов. Не забывайте его добавлять — необработанные ошибки промисов в Node.js приводят к падению процесса.
async/await — синтаксический сахар над промисами
async/await появился в ES2017 и делает асинхронный код похожим на синхронный. Под капотом это те же промисы, просто записанные иначе.
// Функция с async всегда возвращает промис
async function getUser(id) {
// await «ждёт» выполнения промиса
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`Пользователь ${id} не найден`);
}
return response.json(); // автоматически оборачивается в Promise.resolve()
}
// Вызов async-функции
async function main() {
const user = await getUser(1);
console.log(user.name);
}
main().catch(console.error);
await работает только внутри async-функции (или на верхнем уровне модуля в современных окружениях). Попытка использовать await вне async-функции — синтаксическая ошибка.
Обработка ошибок в async/await
Для обработки ошибок используется привычный try/catch:
async function loadUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// throw внутри async-функции — это rejected Promise
throw new Error(`Сервер вернул ${response.status}`);
}
const user = await response.json();
const posts = await fetch(`/api/posts?author=${user.id}`).then(r => r.json());
return { user, posts };
} catch (err) {
// Обрабатываем и сетевые ошибки, и ошибки парсинга, и наши throw
console.error('Ошибка загрузки:', err.message);
// Можно пробросить дальше или вернуть fallback
return { user: null, posts: [] };
}
}
// Использование
const { user, posts } = await loadUserData(42);
Если нужно обрабатывать ошибки отдельно для каждого await, оберните каждый вызов в свой try/catch или используйте паттерн с деструктуризацией:
// Паттерн «Go-style» для отдельной обработки каждой ошибки
async function safeAwait(promise) {
try {
const result = await promise;
return [null, result];
} catch (err) {
return [err, null];
}
}
async function example() {
const [userErr, user] = await safeAwait(fetchUser(1));
if (userErr) {
console.log('Не удалось загрузить пользователя:', userErr.message);
return;
}
const [postsErr, posts] = await safeAwait(fetchPosts(user.id));
if (postsErr) {
console.log('Посты недоступны, показываем только профиль');
}
return { user, posts: posts || [] };
}
Параллельное выполнение: Promise.all, Promise.allSettled, Promise.race
Частая ошибка — последовательно ждать несколько независимых запросов. Если они не зависят друг от друга, запускайте их параллельно.
// ПЛОХО: запросы выполняются последовательно (2 секунды вместо 1)
async function slowVersion() {
const user = await fetchUser(1); // 1 сек
const config = await fetchConfig(); // ещё 1 сек
return { user, config };
}
// ХОРОШО: запросы выполняются параллельно (~1 секунда)
async function fastVersion() {
const [user, config] = await Promise.all([
fetchUser(1),
fetchConfig()
]);
return { user, config };
}
Promise.all — ждёт все промисы. Если хоть один завершится ошибкой — весь Promise.all отклонится.
Promise.allSettled — ждёт все промисы и возвращает результаты каждого, независимо от успеха или ошибки:
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Запрос ${index} успешен:`, result.value);
} else {
console.log(`Запрос ${index} упал:`, result.reason.message);
}
});
Promise.race — возвращает результат первого завершившегося промиса (успех или ошибка). Полезно для реализации таймаутов:
// Таймаут для fetch через Promise.race
function fetchWithTimeout(url, ms = 5000) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Таймаут: ${ms}мс истекло`)), ms)
);
return Promise.race([fetch(url), timeout]);
}
// Использование
try {
const response = await fetchWithTimeout('/api/data', 3000);
const data = await response.json();
} catch (err) {
console.error(err.message); // "Таймаут: 3000мс истекло"
}
Типичные ошибки при работе с async/await
1. Забыть await — функция вернёт промис, а не значение:
// ОШИБКА: user — это Promise, а не объект
async function bad() {
const user = fetchUser(1); // пропущен await!
console.log(user.name); // undefined
}
// ПРАВИЛЬНО:
async function good() {
const user = await fetchUser(1);
console.log(user.name); // "Иван"
}
2. async в forEach — итерация не дожидается завершения:
// ОШИБКА: forEach не знает об async, не ждёт завершения
const ids = [1, 2, 3];
ids.forEach(async (id) => {
await processUser(id); // порядок не гарантирован, код после forEach не ждёт
});
// ПРАВИЛЬНО (последовательно):
for (const id of ids) {
await processUser(id);
}
// ПРАВИЛЬНО (параллельно):
await Promise.all(ids.map(id => processUser(id)));
3. Не обработать ошибку — в Node.js приведёт к UnhandledPromiseRejection:
// ПЛОХО: ошибка из async-функции нигде не обрабатывается
async function riskyOperation() {
throw new Error('Что-то сломалось');
}
riskyOperation(); // UnhandledPromiseRejection!
// ХОРОШО:
riskyOperation().catch(err => console.error(err.message));
// или
await riskyOperation(); // внутри другой async-функции с try/catch
Практический пример: загрузка данных с повторными попытками
Финальный пример — функция с retry-логикой, которая пригодится в реальных проектах:
/**
* Выполняет async-функцию с повторными попытками при ошибке
* @param {Function} fn - async-функция для выполнения
* @param {number} retries - максимум попыток
* @param {number} delayMs - задержка между попытками (мс)
*/
async function withRetry(fn, retries = 3, delayMs = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (err) {
console.warn(`Попытка ${attempt}/${retries} не удалась: ${err.message}`);
if (attempt === retries) {
throw new Error(`Все ${retries} попытки исчерпаны. Последняя ошибка: ${err.message}`);
}
// Экспоненциальная задержка: 1с, 2с, 4с...
await new Promise(resolve => setTimeout(resolve, delayMs * 2 ** (attempt - 1)));
}
}
}
// Использование
async function main() {
try {
const data = await withRetry(
() => fetch('/api/unstable-endpoint').then(r => r.json()),
3, // 3 попытки
500 // начальная задержка 500мс
);
console.log('Данные получены:', data);
} catch (err) {
console.error('Не удалось получить данные:', err.message);
}
}
main();
Итог: шпаргалка
- new Promise(resolve, reject) — создать промис вручную
- .then() / .catch() / .finally() — цепочки обработки
- async function — всегда возвращает промис
- await — ждёт промис, работает только в async-функции
- try/catch — обработка ошибок в async/await
- Promise.all([]) — параллельно, падает при первой ошибке
- Promise.allSettled([]) — параллельно, возвращает все результаты
- Promise.race([]) — первый завершившийся побеждает
- В
forEachasync не работает как ожидается — используйтеfor...ofилиPromise.all + map - Всегда обрабатывайте ошибки:
.catch()илиtry/catch
