Антон Ларичев

Введение
Асинхронное программирование — один из ключевых навыков JavaScript-разработчика. Долгое время для работы с асинхронным кодом использовались колбэки и промисы, но с появлением async/await в ES2017 код стал значительно чище и понятнее. В этом руководстве разберём всё: от внутреннего устройства до типичных ошибок на практике.
Почему асинхронность важна
JavaScript — однопоточный язык. Это означает, что все операции выполняются последовательно в одном потоке. Если заблокировать поток долгим запросом к серверу, интерфейс зависнет и пользователь ничего не сможет сделать.
Асинхронный код позволяет запускать долгие операции (сетевые запросы, чтение файлов, таймеры) без блокировки основного потока. Пока операция выполняется «в фоне», поток свободен для другой работы.
Event Loop и очередь задач
Под капотом async/await работает Event Loop. Когда встречается await, выполнение функции приостанавливается, а управление возвращается вызывающему коду. Когда промис завершается, функция возобновляется с того места, где была остановлена.
Промисы как основа async/await
Async/await — это синтаксический сахар над промисами. Чтобы понять async/await, нужно разобраться с промисами.
Промис — объект, представляющий результат асинхронной операции. Он может находиться в трёх состояниях: pending (ожидание), fulfilled (выполнен успешно), rejected (отклонён с ошибкой).
// Создание промиса вручную
const fetchUser = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
// Успешно возвращаем пользователя
resolve({ id, name: 'Иван Петров' });
} else {
// Отклоняем с ошибкой при некорректном id
reject(new Error('Некорректный ID пользователя'));
}
}, 1000);
});
};
Синтаксис async/await
Объявление async-функции
Ключевое слово async перед функцией делает её асинхронной. Такая функция всегда возвращает промис, даже если внутри возвращается обычное значение.
// Обычная async-функция
async function getUser(id) {
return { id, name: 'Иван' };
}
// Стрелочная async-функция
const getProduct = async (id) => {
return { id, price: 1500 };
};
// Оба варианта возвращают промис
getUser(1).then(user => console.log(user)); // { id: 1, name: 'Иван' }
Оператор await
await можно использовать только внутри async-функции. Он приостанавливает выполнение до тех пор, пока промис не завершится, и возвращает его результат.
async function loadUserProfile(userId) {
// Ожидаем получения пользователя
const user = await fetchUser(userId);
// Ожидаем получения постов пользователя
const posts = await fetchPosts(user.id);
// Ожидаем получения комментариев к постам
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
}
Обработка ошибок
try/catch с async/await
Ошибки в async/await обрабатываются через стандартный try/catch, что делает код более читаемым по сравнению с цепочками .catch().
async function loadData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (error) {
// Обрабатываем любую ошибку из цепочки await
console.error('Ошибка загрузки данных:', error.message);
throw error; // Пробрасываем ошибку выше при необходимости
} finally {
// Выполняется всегда — удобно для очистки ресурсов
console.log('Загрузка завершена');
}
}
Обработка ошибок для каждого await
Иногда нужно обрабатывать ошибки для каждой операции отдельно:
async function robustLoad(userId) {
// Используем .catch() прямо на промисе для локальной обработки
const user = await fetchUser(userId).catch(() => null);
if (!user) {
return { error: 'Пользователь не найден' };
}
const posts = await fetchPosts(user.id).catch(() => []);
return { user, posts };
}
Параллельное выполнение
Одна из частых ошибок — последовательное выполнение независимых запросов. Если запросы не зависят друг от друга, их нужно запускать параллельно.
Promise.all
Promise.all запускает все промисы одновременно и ждёт, пока все завершатся. Если хотя бы один отклоняется — отклоняется весь результат.
async function loadDashboard(userId) {
// Неэффективно: запросы выполняются последовательно (3 секунды)
// const user = await fetchUser(userId);
// const stats = await fetchStats(userId);
// const notifications = await fetchNotifications(userId);
// Эффективно: запросы выполняются параллельно (1 секунда)
const [user, stats, notifications] = await Promise.all([
fetchUser(userId),
fetchStats(userId),
fetchNotifications(userId),
]);
return { user, stats, notifications };
}
Promise.allSettled
Promise.allSettled ждёт завершения всех промисов, независимо от того, выполнены они или отклонены. Полезен когда нужны все результаты, даже при частичных ошибках.
async function loadOptionalData(userId) {
const results = await Promise.allSettled([
fetchUser(userId),
fetchRecommendations(userId), // Необязательные данные
fetchBadges(userId), // Необязательные данные
]);
// Проверяем статус каждого результата
const [userResult, recsResult, badgesResult] = results;
return {
user: userResult.status === 'fulfilled' ? userResult.value : null,
recommendations: recsResult.status === 'fulfilled' ? recsResult.value : [],
badges: badgesResult.status === 'fulfilled' ? badgesResult.value : [],
};
}
Promise.race и Promise.any
async function fetchWithTimeout(url, timeoutMs) {
// Возвращает результат того промиса, который завершится первым
const result = await Promise.race([
fetch(url),
new Promise((_, reject) =>
// Отклоняем через заданное время
setTimeout(() => reject(new Error('Таймаут запроса')), timeoutMs)
),
]);
return result;
}
Частые ошибки
Забытый await
Отсутствие await — одна из самых распространённых ошибок. Без него функция вернёт промис, а не значение.
async function wrongExample() {
// Ошибка: переменная содержит промис, а не пользователя
const user = fetchUser(1);
console.log(user.name); // undefined
}
async function correctExample() {
// Правильно: ожидаем выполнения промиса
const user = await fetchUser(1);
console.log(user.name); // 'Иван Петров'
}
await в цикле вместо параллелизма
// Медленно: каждый запрос ждёт предыдущий
async function slowLoad(ids) {
const results = [];
for (const id of ids) {
const user = await fetchUser(id); // Блокирует следующую итерацию
results.push(user);
}
return results;
}
// Быстро: все запросы выполняются параллельно
async function fastLoad(ids) {
const promises = ids.map(id => fetchUser(id));
return await Promise.all(promises);
}
Потеря контекста ошибки
async function badErrorHandling() {
try {
await riskyOperation();
} catch (error) {
// Плохо: теряем оригинальный стек ошибки
throw new Error('Что-то пошло не так');
}
}
async function goodErrorHandling() {
try {
await riskyOperation();
} catch (error) {
// Хорошо: сохраняем оригинальную ошибку через cause
throw new Error('Ошибка в riskyOperation', { cause: error });
}
}
Заключение
Async/await значительно упрощает работу с асинхронным кодом в JavaScript. Ключевые моменты для запоминания: async-функция всегда возвращает промис, await работает только внутри async-функции, независимые операции стоит запускать параллельно через Promise.all, а ошибки обрабатывать через try/catch.
Освоив эти паттерны, вы сможете писать чистый, читаемый асинхронный код и избегать классических ошибок производительности.






Комментарии
0