JavaScript — однопоточный язык. Это значит, что он может выполнять только одну операцию за раз. Но как тогда работают запросы к серверу, таймеры и чтение файлов? Всё дело в асинхронности. Раньше приходилось использовать колбэки, потом появились промисы, а сегодня есть async/await — синтаксический сахар, который делает асинхронный код похожим на обычный синхронный. Если вы до сих пор путаетесь в then и catch, эта статья для вас.
Что такое асинхронность и зачем она нужна
Представьте, что вы в кафе. Официант принимает заказ, относит его на кухню и... стоит и ждёт, пока повар приготовит блюдо. В это время другие посетители не могут сделать заказ. Это синхронный подход: одна задача блокирует всё остальное.
В реальной жизни официант не ждёт — он идёт к следующему столику, а когда блюдо готово, просто приносит его. Это асинхронность. В JavaScript так же: пока один запрос к серверу выполняется, код продолжает работать.
Раньше для этого использовали функции обратного вызова (колбэки). Но если вложенных колбэков становилось много, код превращался в «ад пирамид» — его было тяжело читать и поддерживать. Потом придумали промисы, а затем — async/await.
Синтаксис async и await: первый взгляд
Ключевое слово async ставится перед функцией. Оно говорит: «Эта функция всегда возвращает промис». Даже если вы вернёте просто число, оно обернётся в промис.
Ключевое слово await можно использовать только внутри async-функции. Оно заставляет JavaScript ждать, пока промис выполнится, и возвращает его результат. При этом код выглядит как синхронный.
// Обычная функция
function getData() {
return fetch('https://api.example.com/data');
}
// Асинхронная функция
async function getDataAsync() {
return fetch('https://api.example.com/data');
}
// Использование
async function main() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
}
main();
Обратите внимание: мы не используем .then(). Вместо этого просто пишем await и получаем результат. Код читается сверху вниз, как обычный синхронный.
await, она обязана быть объявлена как async. Иначе будет синтаксическая ошибка.
Обработка ошибок: try/catch вместо catch
С промисами мы использовали .catch() в конце цепочки. С async/await всё проще: обычный try...catch, как в синхронном коде.
async function fetchUser(id) {
try {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error('Пользователь не найден');
}
const user = await response.json();
return user;
} catch (error) {
console.error('Ошибка:', error.message);
// Можно вернуть запасной вариант или пробросить ошибку дальше
throw error;
}
}
Важно: await выбрасывает исключение, если промис отклонён. Поэтому try...catch ловит и сетевые ошибки, и ошибки в логике.
Последовательное и параллельное выполнение
Одна из главных ошибок новичков — запускать несколько независимых промисов последовательно, когда их можно запустить параллельно. Это замедляет программу.
Сравните два подхода:
// Плохо: ждём по очереди
async function sequential() {
const user = await fetch('/user');
const posts = await fetch('/posts');
const comments = await fetch('/comments');
return { user, posts, comments };
}
// Хорошо: запускаем все сразу
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetch('/user'),
fetch('/posts'),
fetch('/comments')
]);
return { user, posts, comments };
}
Во втором случае все три запроса уйдут на сервер одновременно. Время выполнения сократится в три раза (если запросы независимы).
Promise.all(), когда результаты промисов не зависят друг от друга. Если хотя бы один промис отклонится, Promise.all сразу выбросит ошибку. Для более гибкой обработки есть Promise.allSettled().
Сравнение подходов: колбэки, промисы, async/await
Чтобы понять преимущества async/await, посмотрим на таблицу.
| Подход | Читаемость | Обработка ошибок | Отладка |
|---|---|---|---|
| Колбэки | Низкая (ад пирамид) | Сложная, легко пропустить | Трудно ставить точки останова |
| Промисы | Средняя, цепочки then | Через .catch() | Лучше, но цепочки мешают |
| async/await | Высокая, как синхронный код | try/catch, как обычно | Отлично, точки останова работают |
Как видите, async/await выигрывает по всем параметрам. Неудивительно, что это стандарт современного JavaScript.
Практические советы и подводные камни
Вот несколько вещей, которые стоит запомнить, чтобы не наступить на грабли.
- Не забывайте про await. Если вы напишете
fetch('/data')без await, то получите промис, а не данные. Иногда это приводит к неочевидным багам. - Избегайте await в циклах. Если нужно выполнить несколько асинхронных операций по очереди, лучше собрать промисы в массив и использовать
Promise.all. Но если порядок важен — цикл с await оправдан. - Обрабатывайте ошибки. Не оставляйте
try...catchпустым. Логируйте или пробрасывайте ошибку дальше. - Помните, что async-функция возвращает промис. Даже если вы возвращаете значение, снаружи это промис. Используйте
.then()илиawaitпри вызове.
console.log с метками времени или инструменты разработчика в браузере. Они позволяют ставить точки останова внутри async-функций и отслеживать цепочку вызовов.
Пример из реальной жизни: загрузка данных пользователя
Допустим, нам нужно загрузить профиль пользователя, затем его посты, а потом комментарии к первому посту. Всё это — асинхронные запросы.
async function loadUserProfile(userId) {
try {
// Загружаем профиль
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
// Загружаем посты пользователя
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const posts = await postsResponse.json();
// Если есть посты, загружаем комментарии к первому
let comments = [];
if (posts.length > 0) {
const commentsResponse = await fetch(`/api/posts/${posts[0].id}/comments`);
comments = await commentsResponse.json();
}
return { user, posts, comments };
} catch (error) {
console.error('Не удалось загрузить профиль:', error);
throw error;
}
}
// Использование
loadUserProfile(123)
.then(data => console.log(data))
.catch(err => console.error(err));
Код читается сверху вниз, как обычный сценарий. Мы точно знаем, в каком порядке выполняются запросы, и легко можем добавить логику между ними.
Что в итоге
Async/await — это не просто синтаксический сахар. Это способ писать асинхронный код, который легко читать, отлаживать и поддерживать. Он не заменяет промисы, а работает поверх них. Вы по-прежнему используете Promise.all, Promise.race и другие методы, но теперь без громоздких цепочек .then().
Начните с малого: перепишите одну асинхронную функцию с промисов на async/await. Вы сразу почувствуете разницу. Главное — не забывайте про try...catch и параллельное выполнение там, где это возможно.
Асинхронность в JavaScript перестала быть болью. Пользуйтесь.



