Async/Await в JavaScript

Пишем асинхронный код как обычный синхронный

Async/Await в JavaScript

Когда вы нажимаете кнопку на сайте, отправляете форму или грузите данные с сервера, всё это запускает асинхронные процессы. Без асинхронности сайты зависали бы на каждом шагу: они бы ждали, пока придёт ответ с сервера, потом — пока загрузится картинка и пока не пройдёт таймер.

Чтобы всё работало чётко, в JavaScript используют инструменты асинхронности — колбэки, промисы и конструкцию async/await

Сегодня разберёмся, как работает асинхронность в JS, вспомним, что такое промисы, и как писать понятный код с async/await.

Введение в асинхронное программирование

Код может быть синхронным и асинхронным. По умолчанию JavaScript исполняется строго последовательно, строка за строкой. Это называется синхронным выполнением: пока одна строка не отработает, следующая не запустится.

Но в реальной жизни какие-то штуки не происходят сразу же: отправка формы, запрос на сервер, чтение файла, загрузка картинки — всё это занимает время. И пользователь не должен смотреть в зависший экран, пока наш код что-то там делает.

Тогда используется асинхронность — подход, при котором операция может выполняться в фоне, а остальная программа продолжает работать. Мы просто «подпишемся» на результат, и когда он будет готов — получим уведомление и сможем работать с результатом. 

Что такое асинхронный JavaScript

JavaScript — однопоточный язык. Это значит, что он выполняет код по очереди, одна операция за другой. Но задачи могут быть долгими: загрузка данных, таймеры, работа с сетью. Если выполнять их строго по очереди, приложение зависнет.

Чтобы этого избежать, в JS есть асинхронные механизмы. Они позволяют запускать «долгие» задачи, не блокируя остальной код. Например, можно отправить запрос на сервер, и пока он не вернулся, продолжить выполнение программы.

Асинхронный JavaScript — это не отдельный режим работы, а набор инструментов и механизмов, которые позволяют откладывать выполнение кода и реагировать на события тогда, когда они действительно произойдут.

Как JavaScript удаётся управлять задачами, оставаясь при этом однопоточным? Всё дело в так называемом цикле событий — Event Loop. Это механизм, который управляет выполнением асинхронных операций.

Как работает Event Loop

Для начала нужно понять, как работает Event Loop — движок, который управляет порядком выполнения кода. JavaScript работает в однопоточном режиме, но при этом умудряется выполнять и синхронные, и асинхронные задачи, создавая ощущение многозадачности.

Event Loop — это не часть самого JavaScript, а механизм среды исполнения. Сам по себе JS — просто язык, и он не умеет делать setTimeout, fetch или слушать события. Эти штуки реализуются внешними окружениями — браузером, Node.js и так далее. Именно они предоставляют доступ к Web API и запускают Event Loop — бесконечный цикл, который управляет очередями задач и следит, что когда нужно исполнить.

Внутри Event Loop есть несколько важных компонентов.

  • Call Stack — стек вызовов, куда попадают функции для выполнения. Он работает строго по порядку: верхняя задача должна завершиться, чтобы перешли к следующей.
  • Web API — специальная зона, куда отправляются асинхронные операции — setTimeout, fetch, обработчики событий и другие.
  • Task Queue — очередь задач, в которую Web API кладут готовые задачи (например, колбэк от таймера).
  • Microtask Queue — очередь микротасок (или микрозадач). Сюда попадают обработчики .then(), .catch() и .finally() от разрешённых промисов, а также задачи, добавленные через queueMicrotask().

Async/Await в JavaScript
Источник: https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif

Event Loop работает так:

  1. Выполняем весь синхронный код из стека (то, что идёт напрямую).
  2. Асинхронные операции (setTimeout, fetch, события и так далее) отправляем во внешние API среды исполнения (например, браузера).
  3. Как только стек опустел — выполняем все микротаски (Promise.then(), queueMicrotask() и такое прочее). Все до одной.
  4. После микротасок берём одну задачу из очереди (setTimeout, setInterval, обработчики событий).
  5. Event Loop начинает цикл заново — с первого пункта.

Поэтому промис срабатывает раньше, чем setTimeout, даже если стоит после в коде: микротаски идут с приоритетом.

Чтобы лучше во всём разобраться, можно зайти на интерактивный сайт latentflip, вставить туда JS-код и посмотреть, в каком порядке что будет выполняться:

Async/Await в JavaScript

Что такое синхронная система

Синхронный код — как очередь в магазине с одной кассой: одного человека обслуживают — остальные ждут. Пока первая задача не завершится, следующая не начнётся. В JavaScript синхронный код реализован через стек вызовов. Там каждая новая функция кладётся сверху и выполняется до конца, прежде чем мы можем перейти к следующей.

function step1() {
  console.log("Шаг 1");
}
function step2() {
  console.log("Шаг 2");
  step1();
  console.log("Шаг 3");
}
step2();

Всё последовательно и понятно:

Async/Await в JavaScript
  1. Сначала вызывается step2() — кладётся в стек. 
  2. Внутри неё выполняется console.log("Шаг 2"), затем вызывается step1() — она тоже попадает в стек, выполняется и выводит «Шаг 1». 
  3. После этого step1() завершается и удаляется из стека, выполнение возвращается в step2() и она завершает оставшееся — «Шаг 3».

Что такое асинхронная система

Асинхронный код — как заказ в кофейне: заказал, получил чек, пошёл по делам, а когда кофе готов, бариста несёт кофе. Код не тормозит, а продолжает работать, пока фоновая задача не завершится.

Асинхронные задачи — это setTimeout, fetch, события, промисы и другие. Они сначала уходят во внешние Web API (которые предоставляет браузер или Node.js) и уже после завершения возвращаются в очередь задач, откуда Event Loop отправляет их обратно в основной поток для выполнения.

Если запустим такой код:

console.log("1. Идём в кофейню");
setTimeout(() => {
  console.log("3. Получаем кофе");
}, 2000);
console.log("2. Смотрим мемы в телефоне");

то в консоли увидим такое:

Async/Await в JavaScript
  1. Сначала выполняется строка console.log("1. Идём в кофейню") — это синхронный код, он сразу летит в консоль. 
  2. Затем JS доходит до setTimeout() и отправляет эту задачу во внешнее окружение (Web API). Таймер начинает отсчёт, но JavaScript не ждёт — он продолжает выполнять следующий синхронный console.log("2. Смотрим мемы в телефоне").
  3. Когда проходит 2 секунды, колбэк из setTimeout() возвращается в Task Queue и ждёт, пока стек вызовов освободится. Event Loop следит за этим: как только стек пуст, он берёт задачу из очереди и запускает её — появляется сообщение "3. Получаем кофе".

👉 Кстати, это частый вопрос на собесах, когда дают кусок асинхронного кода и просят объяснить, в каком порядке он выполнится и почему. Поэтому важно не просто писать async/await и setTimeout на автомате, а реально понимать, как работает Event Loop.

Асинхронные функции

Для начала напомним про промисы. Promise — это объект, который представляет результат асинхронной операции. Он может быть в одном из трёх состояний: ожидание (pending), успешно выполнен (fulfilled) или ошибка (rejected). С промисами мы можем обрабатывать результат, когда он будет готов, — с помощью .then() и .catch().

// пример простого промиса
const promise = new Promise((resolve, reject) => {
  const success = true;
  if (success) {
    resolve("Всё получилось!");
  } else {
    reject("Что-то пошло не так...");
  }
});
promise
  .then(result => console.log(result))
  .catch(error => console.error(error));

Но читать такие цепочки .then().catch() бывает не очень удобно — особенно если логика сложная. И чтобы писать асинхронный код проще и читабельнее, в JavaScript добавили надстройку над промисами — async/await. Она делает асинхронный код похожим на обычный синхронный, без вложенных цепочек.

Синтаксис Async/Await

async/await — это синтаксический сахар над промисами. То есть под капотом всё работает на тех же самых промисах, просто код выглядит чище, читается легче и не дробится на .then().then().catch(). С async/await код можно читать сверху вниз, без вложенностей и цепочек.

Выглядит это так:

async function getData() {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    console.log("Полученные данные:", data);
  } catch (error) {
    console.error("Ошибка при загрузке:", error);
  }
}

Разберём подробнее, что тут происходит.

  • Async перед функцией. Когда перед объявлением функции мы добавляем async, то говорим JavaScript: «Эта функция будет работать асинхронно и вернёт промис». Даже если внутри мы просто возвращаем строку, это всё равно будет обёрнуто в Promise.resolve().
  • Await. Ключевое слово await можно использовать только внутри async-функции (хотя на самом деле есть нюансы: в последних версиях JS его можно использовать и на верхнем уровне в модулях). await приостанавливает выполнение кода до тех пор, пока промис не вернёт результат — успех (resolve) или ошибку (reject). Это и даёт тот самый эффект «синхронного» чтения асинхронного кода. Важно: если промис завершится с ошибкой, await выбросит исключение.
  • Try/catch для обработки ошибок. Когда используем await, нужно обрабатывать возможные ошибки, особенно при запросах к серверу. Вместо .catch() на каждый промис используем старый добрый try/catch, как в обычном JavaScript.

Параметры

Функции с async работают как обычные: мы передаём им параметры и получаем результат — только результат будет внутри промиса.

async function greet(name) {
  return `Привет, ${name}!`;
}
greet("Чарли").then(msg => console.log(msg)); // Привет, Чарли!
Async/Await в JavaScript

Дальше посмотрим, как именно работает async/await под капотом и чем он отличается от обычных промисов с .then().

Как работает Async/Await

Итак, async/await — это по сути промис. Когда мы пишем await, JavaScript ставит выполнение на паузу именно в этой точке — пока промис не вернёт результат. Как только промис выполнится, интерпретатор продолжает с того же места. 

Всё работает благодаря Event Loop: пока await ждёт, движок освобождает стек вызовов, обрабатывает другие задачи, а когда результат готов — возвращается.

Примеры использования Async/Await

Допустим, нам нужно получить профиль пользователя с сервера, потом его посты, потом комментарии к первому посту:

// Объявляем асинхронную функцию
async function getData() {
  try {
    // Ждём, пока сервер вернёт данные о пользователе
    const user = await fetchUser();
    // Когда получили пользователя — запрашиваем его посты
    const posts = await fetchPosts(user.id);
    // Берём первый пост и запрашиваем к нему комментарии
    const comments = await fetchComments(posts[0].id);
    // Выводим комментарии в консоль
    console.log(comments);
  } catch (error) {
    // Если на любом этапе произошла ошибка — ловим её здесь
    console.error("Ошибка:", error);
  }
}

Каждая строчка с await — это точка, где выполнение функции приостанавливается, пока не будет получен результат. Поэтому код читается и исполняется сверху вниз, как обычный синхронный алгоритм:

  1. Сначала получили пользователя.
  2. Потом его посты.
  3. Потом комментарии.
  4. В конце вывели всё в консоль или поймали ошибку, если что-то пошло не так.

Промисы против Async/Await

Оба способа работают с одной и той же логикой — промисами .async/await просто делает эту работу более читаемой и похожей на синхронный код. Это полезно, когда нужно шаг за шагом обрабатывать данные — без вложенности .then() и .catch(). И у каждого подхода есть свои плюсы и ситуации, когда он более уместен.

Допустим, есть такой код на чистых промисах:

fetchUser()
  .then(user => {
    return fetchPosts(user.id);
  })
  .then(posts => {
    return fetchComments(posts[0].id);
  })
  .then(comments => {
    console.log(comments);
  })
  .catch(error => {
    console.error("Ошибка:", error);
  });

Здесь к каждому промису мы цепляем .then(). Всё работает, но при большом количестве шагов это может превратиться в кучу вложенных стрелочек, особенно если будет логика внутри .then().

А вот то же самое с async/await:

async function getData() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);

    console.log(comments);
  } catch (error) {
    console.error("Ошибка:", error);
  }
}

Читается как обычный синхронный код.

Когда и что использовать:

  • Если простая последовательность действий — лучше async/await.
  • Если нужно обработать массив промисов (Promise.all, Promise.race) или делать цепочку с промежуточными .then() — удобнее использовать промисы напрямую.
  • В библиотеках и низкоуровневом коде всё ещё часто используют чистые промисы.

👉 На собесе также могут дать код с промисами и попросить переписать на async/await — или наоборот. 

Обработка ошибок

В асинхронном коде может быть куча ошибок: сеть отвалилась, сервер вернул 500, данные пришли не туда — и это нормально. Главное — всё правильно отловить и не сломать интерфейс.

Как использовать ключевые слова Try и Catch

Ключевые слова try/catch — это базовый механизм для отлова ошибок в JavaScript. Внутри try { ... } пишем потенциально опасный код, а внутри catch (err) { ... } — что делать, если что-то пошло не так.

try {
  // потенциально опасная операция
  const data = JSON.parse('💩');
  console.log(data);
} catch (err) {
 // что делать, если всё плохо
  console.error("Ошибка при разборе JSON:", err.message);
}

Если JSON валидный — всё ок. Если нет — приложение не падает, а просто ругается.

Обработка ошибок с использованием Async/Await

Когда мы используем async/await, промисы больше не обрабатываются через .catch(). Ошибка летит прямо в try/catch, как будто это обычный синхронный код.

Выглядит это так:

async function getUser() {
  try {
    // Отправляем асинхронный запрос на сервер
    const response = await fetch("https://api.example.com/user");
    // Проверяем, успешен ли ответ (не 404, не 500 и так далее)
    if (!response.ok) {
      // Если нет — выбрасываем ошибку вручную
      throw new Error("Сервер вернул ошибку: " + response.status);
    }
    // Парсим тело ответа как JSON (тоже асинхронно)
    const user = await response.json();
    // Выводим полученные данные в консоль
    console.log("Пользователь:", user);
  } catch (error) {
    // Если на любом этапе возникла ошибка — попадаем сюда
    console.error("Произошла ошибка:", error.message);
  }
}

Иногда удобнее оборачивать каждый шаг отдельно, особенно если один шаг не должен ломать весь процесс.

async function loadData() {
  let user, posts;
  try {
    // Пытаемся получить пользователя
    user = await fetchUser();
  } catch (e) {
    // Если ошибка — предупреждаем и выходим из функции
    console.warn("Не удалось получить пользователя:", e.message);
    return;
  }
  try {
    // Пытаемся получить посты пользователя
    posts = await fetchPosts(user.id);
  } catch (e) {
    // Если не получилось — предупреждаем, но продолжаем работу
    console.warn("Посты не загрузились:", e.message);
   // запасной план: пустой массив
    posts = []; 
  }
  // Показываем, что получилось
  console.log("Посты:", posts);
}

Такой подход удобен, когда одна часть не должна стопорить всё. Если получили пользователя — отлично. Не получили посты — ничего страшного, идём дальше.

Преимущества Async/Await

async/await появился не просто так — он реально делает жизнь разработчика легче. Особенно когда код начинает разрастаться, обрастать вложенностями и условиями. 

Читаемость кода

Когда мы работаем с then().then().then() — вроде бы всё понятно, но если нужно добавить логику, условия или обработку ошибок, то цепочка превращается в спагетти. А с async/await код выглядит проще:

// с промисами
fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error("Ошибка:", error));
// async/await
async function getData() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    console.log(comments);
  } catch (e) {
    console.error("Ошибка:", e);
  }
}

Читается сверху вниз, как обычный пошаговый алгоритм.

Упрощение обработки ошибок

С промисами ошибки приходится отлавливать либо в .catch(), либо передавать их дальше по цепочке. А если хотим отдельно обработать ошибки на каждом этапе, то тут всё ещё сложнее. С async/await мы просто используем try/catch, как в обычном синхронном коде.

Снижение вложенности

Каждый .then() с условиями и циклами добавляет ещё один уровень вложенности. А async/await помогает этого избегать:

// большая вложенность
getUser().then(user => {
  if (user) {
    getSettings(user.id).then(settings => {
      if (settings.theme === "dark") {
        console.log("Тёмная тема активна");
      }
    });
  }
});

А вот с async/await всё выглядит проще, хотя логика точно такая же:

async function checkTheme() {
  const user = await getUser();
  if (!user) return;
  const settings = await getSettings(user.id);
  if (settings.theme === "dark") {
    console.log("Тёмная тема активна");
  }
}

Практические примеры и лучшие практики

Когда мы работаем с async/await, то можем запускать все асинхронные задачи либо последовательно, либо параллельно. Что выбрать — зависит от логики:

  • Если одна задача зависит от результата другой — запускаем последовательно.
  • Если задачи независимы, можно запускать параллельно — это сэкономит время.

Разберём оба случая.

Параллельное выполнение асинхронных задач

Если несколько задач можно выполнить одновременно (и они не зависят друг от друга), тогда нет смысла ждать их одну за другой. Лучше запускать их параллельно — это быстрее и эффективнее.

Допустим, мы грузим профиль пользователя, список постов и комментарии — и они не зависят друг от друга.

Тогда пишем так:

async function loadData() {
  // Запускаем все три запроса одновременно
  const userPromise = fetchUser();
  const postsPromise = fetchPosts();
  const commentsPromise = fetchComments();
  // Ждём, когда всё завершится
  const user = await userPromise;
  const posts = await postsPromise;
  const comments = await commentsPromise;
  console.log({ user, posts, comments });
}

Такой подход ускоряет выполнение кода, потому что запросы выполняются параллельно, а не ждут друг друга. Итоговое время — самое долгое из трёх, а не сумма всех.

Последовательное выполнение асинхронных задач

А вот если один шаг зависит от результата предыдущего, тогда ждём по очереди.

Например, сначала нужно получить профиль пользователя, потом по его ID — посты, а уже потом — комментарии к первому посту. В этом случае пишем так:

async function loadData() {
  // получаем пользователя
  const user = await fetchUser();         
  // загружаем посты по user.id     
  const posts = await fetchPosts(user.id);     
  // комментарии к первому посту
  const comments = await fetchComments(posts[0].id); 
  console.log({ user, posts, comments });
}

Здесь уже нельзя запустить всё параллельно, потому что каждое действие зависит от предыдущего. Если попытаемся ускорить, получим undefined, ошибку или неправильные данные.

Спецификации и совместимость с браузерами

Асинхронные функции появились в спецификации ECMAScript 2017 (ES8) и с тех пор стали стандартом в современном JavaScript. На сегодняшний день async/await поддерживаются во всех современных браузерах.

Async/Await в JavaScript

Если по какой-то причине нужно поддерживать IE11 или устаревшие окружения, есть два варианта:

  1. Использовать Babel для транспиляции async/await в обычные промисы.
  2. Или полностью отказаться от async/await и писать на промисах вручную.

Но в 2025 году этого почти не требуется — большинство пользователей уже давно на современных версиях браузеров.

Вам слово

Приходите к нам в соцсети поделиться своим мнением об асинхронном программировании и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте: шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!

Обложка:

Алексей Сухов

Корректор:

Елена Грицун

Вёрстка:

Кирилл Климентьев

Соцсети: Юлия Зубарева
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
А вы читали это?
hard