JavaScript async/await: паттерны, которые стоит знать
Асинхронное программирование — основа современного JavaScript, а async/await сделал его гораздо более читаемым. Но многие разработчики всё ещё сталкиваются с типичными проблемами: необработанные отклонения промисов, медленные последовательные запросы или запутанная обработка ошибок. В этой статье мы рассмотрим практические паттерны async/await, которые можно сразу применить, чтобы писать более чистый и эффективный код.
1. Базовый синтаксис async/await
Асинхронная функция всегда возвращает Promise. Ключевое слово await приостанавливает выполнение до тех пор, пока Promise не завершится, а затем возвращает его разрешённое значение. Если Promise отклоняется, выбрасывается ошибка, которую можно перехватить с помощью try/catch.
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
}
}
Это основа. Но в реальных сценариях часто требуются более тонкие подходы.
2. Грамотная обработка ошибок
Одно из главных преимуществ async/await — возможность использовать try/catch как для синхронных, так и для асинхронных ошибок. Однако будьте осторожны с несколькими вызовами await в одном блоке try: если первый завершится ошибкой, последующие не выполнятся.
Для большей детализации можно обернуть каждый await в собственный try/catch или использовать вспомогательную функцию, чтобы избежать повторяющихся блоков try/catch.
async function getUser(id) {
try {
const user = await fetchUser(id);
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}
3. Параллельное выполнение промисов
Последовательное ожидание нескольких независимых промисов — распространённая ошибка производительности. Если вам нужны результаты из нескольких источников, используйте Promise.all() для их одновременного выполнения.
// Последовательно: медленно
const user = await fetchUser();
const posts = await fetchPosts();
// Параллельно: быстро
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
Используйте Promise.allSettled(), когда нужно дождаться завершения всех промисов, независимо от того, разрешились они или отклонились. Это полезно, если вы хотите собрать результаты, даже если некоторые из них завершились ошибкой.
4. Последовательное выполнение, когда важен порядок
Иногда элементы нужно обрабатывать по порядку, например, когда каждый шаг зависит от предыдущего. В этом случае используйте цикл for...of с await вместо forEach (который не ждёт асинхронные колбэки).
for (const item of items) {
await processItem(item);
}
Если нужно ограничить параллелизм, рассмотрите библиотеки вроде p-limit или реализуйте простую очередь.
5. Как избежать ловушки async/await в циклах
Использование await внутри Array.prototype.forEach — классическая ошибка: forEach не ждёт асинхронный колбэк, поэтому цикл завершается до того, как промисы разрешатся. Всегда используйте for...of или map с Promise.all.
6. Async/await с таймаутами
Чтобы предотвратить зависание запросов, можно устроить гонку промиса с таймаутом.
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
await Promise.race([fetchData(), timeout(5000)]);
Этот паттерн удобен для вызовов API, которые могут зависнуть.
7. Асинхронные генераторы и for await...of
Асинхронные генераторы позволяют асинхронно создавать поток значений. Их можно обрабатывать с помощью for await...of.
async function* fetchPages() {
let page = 1;
while (true) {
const data = await fetch(`/api/items?page=${page}`);
if (data.length === 0) break;
yield data;
page++;
}
}
for await (const page of fetchPages()) {
console.log(page);
}
Это отлично подходит для API с пагинацией или потоковых данных.
8. Типичные ошибки и как их избежать
- Забыли await: Вызов асинхронной функции без await возвращает Promise, и ошибки могут остаться необработанными.
- Использование await в неасинхронных функциях: Синтаксическая ошибка. Всегда помечайте функцию как async.
- Чрезмерное использование последовательных await: По возможности используйте Promise.all.
- Отсутствие обработки отклонений: Всегда добавляйте .catch или try/catch.
9. Лучшие практики для чистого асинхронного кода
- Предпочитайте async/await вместо чистых промисов для читаемости.
- Используйте Promise.all для независимых операций.
- Обрабатывайте ошибки на подходящем уровне.
- Держите асинхронные функции небольшими и сфокусированными.
- Используйте async/await в циклах с осторожностью; выбирайте правильный паттерн.
FAQ
В чём разница между async/await и промисами?
Async/await — это синтаксический сахар над промисами, который делает асинхронный код похожим на синхронный. Он упрощает цепочки и обработку ошибок с помощью try/catch.
Можно ли использовать await внутри цикла forEach?
Нет, forEach не ждёт асинхронные колбэки. Вместо этого используйте for...of или Promise.all с map.
Как запустить несколько асинхронных операций параллельно?
Используйте Promise.all() для их одновременного выполнения и ожидания завершения всех.
При работе с JSON API вы можете быстро форматировать и проверять ответы с помощью нашего JSON Formatter.