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

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

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

import { promises as fs } from 'fs';

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

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

import { promises as fs } from 'fs';

const unionFiles = async (inputPath1, inputPath2, outputPath) => {
  const data1 = await fs.readFile(inputPath1, 'utf-8');
  const data2 = await fs.readFile(inputPath2, 'utf-8');
  await fs.writeFile(outputPath, `${data1}${data2}`);
};

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

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

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

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

import { promises as fs } from 'fs';

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

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

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

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

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

Для продолжения нужно перейти в курс и вступить в него.