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

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

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

import { promises as fs } from 'fs';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  let data1;
  return fs.readFile(inputPath2, '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 = await (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. Пример реального кода из проектов Хекслета