Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Async/Await JS: Асинхронное программирование

Несмотря на все удобства, промисы не являются вершиной эволюции. Вспомним минусы, которые они добавляют:

  • Своя собственная обработка ошибок, которая идёт в обход try/catch. Это значит, что в коде будут появляться оба способа обработки, комбинирующихся в причудливых формах.
  • Иногда бывает нужно передавать данные вниз по цепочке с самых верхних уровней, и с промисами делать это неудобно. Придётся создавать переменные вне промиса.
  • С промисами по-прежнему легко начать создавать вложенность, если специально за этим не следить.

Все эти сложности убираются механизмом async/await, делающим код с промисами ещё более похожим на синхронный! Вспомним нашу задачу по объединению двух файлов. Вот её код:

import fsp from 'fs/promises';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  let data1;
  return fsp.readFile(inputPath1, 'utf-8')
    .then((content) => {
      data1 = content;
    })
    .then(() => fsp.readFile(inputPath2, 'utf-8'))
    .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
};

А теперь посмотрим на этот же код с использованием async/await. Подчеркну, что async/await работает с промисами:

import fsp from 'fs/promises';

const unionFiles = async (inputPath1, inputPath2, outputPath) => {
  // Очень важный момент. Так же как и в примере выше, эти запросы выполняются строго друг за другом
  // (хотя при этом не блокируется программа, это значит, что другой код тоже может выполняться во время этих запросов)
  const data1 = await fsp.readFile(inputPath1, 'utf-8');
  const data2 = await fsp.readFile(inputPath2, 'utf-8');
  await fsp.writeFile(outputPath, `${data1}${data2}`);
};

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

Первое, что мы видим, — это ключевое слово async перед определением функции. Оно означает, что данная функция всегда возвращает промис: const promise = unionFiles(...). Причём теперь не обязательно возвращать результат из этой функции явно, он всё равно станет промисом.

Внутри функции используется ключевое слово await, которое ставится перед вызовом функций, которые, в свою очередь, тоже возвращают промисы. Если результат этого вызова присваивается переменной или константе, то в них записывается результат вызова. Если присвоения нет, как в последнем вызове await, то происходит ожидание выполнения операции без использования её результата.

Асинхронность в данном случае (как и в промисах) гарантирует нам, что программа не блокируется ожидая завершения вызовов, она может продолжать делать что-то еще (но не в этой функции). Но она не гарантирует параллельности. Более того, подряд идущие await в рамках одной функции всегда выполняются строго друг за другом. Проще всего это понимать, если представлять код как цепочку промисов, где каждая следующая операция выполняется внутри then.

А что с обработкой ошибок? Теперь достаточно поставить обычные try/catch и ошибки будут отловлены!

import fsp from 'fs/promises';

const unionFiles = async (inputPath1, inputPath2, outputPath) => {
  try {
    const data1 = await fsp.readFile(inputPath1, 'utf-8');
    const data2 = await fsp.readFile(inputPath2, 'utf-8');
    await fsp.writeFile(outputPath, `${data1}${data2}`);
  } catch (e) {
    console.log(e);
    throw e; // снова бросаем, потому что вызывающий код должен иметь возможность отловить ошибку
  }
};

Однако, при параллельном выполнении промисов не обойтись без функции Promise.all:

const unionFiles = async (inputPath1, inputPath2, outputPath) => {
  // Эти вызовы начинают чтение почти одновременно и не ждут друг друга
  const promise1 = fsp.readFile(inputPath1, 'utf-8');
  const promise2 = fsp.readFile(inputPath2, 'utf-8');
  // Теперь дожидаемся когда они оба завершатся
  // Данные можно сразу разложить
  const [data1, data2] = await Promise.all([promise1, promise2]);
  await fsp.writeFile(outputPath, `${data1}${data2}`);
};

Подводя итог, механизм async/await делает код максимально плоским и похожим на синхронный. Благодаря ему появляется возможность использовать try/catch, и легко манипулировать данными полученными в результате асинхронных операций.

// Код на колбеках
import fs from 'fs';

fs.readFile('./first', 'utf-8', (error1, data1) => {
  if (error1) {
    console.log('boom!');
    return;
  }
  fs.readFile('./second', 'utf-8', (error2, data2) => {
    if (error2) {
      console.log('boom!');
      return;
    }
    fs.writeFile('./new-file', `${data1}${data2}`, (error3) => {
      if (error3) {
        console.log('boom!');
      }
    });
  });
});

// Код на промисах
import fsp from 'fs/promises';

let data1;
fsp.readFile('./first', 'utf-8')
  .then((d1) => {
    data1 = d1;
    return fsp.readFile('./second', 'utf-8');
  })
  .then((data2) => fsp.writeFile('./new-file', `${data1}${data2}`))
  .catch(() => console.log('boom!'));

// Код на async/await
import fsp from 'fs/promises';

// В реальной жизни чтение файлов лучше выполнять параллельно, как в функции unionFiles выше
const data1 = await fsp.readFile('./first', 'utf-8');
const data2 = await fsp.readFile('./second', 'utf-8');
await fsp.writeFile('./new-file', `${data1}${data2}`);

Дополнительные материалы

  1. Пример реального кода из проектов Хекслета

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 120 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
29 сентября 8 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
29 сентября 8 месяцев

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»