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

Цепочка промисов JS: Асинхронное программирование

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

import fs from 'fs';

const unionFiles = (inputPath1, inputPath2, outputPath, cb) => {
  fs.readFile(inputPath1, 'utf-8', (error1, data1) => {
    if (error1) {
      cb(error1);
      return;
    }
    fs.readFile(inputPath2, 'utf-8', (error2, data2) => {
      if (error2) {
        cb(error2);
        return;
      }
      fs.writeFile(outputPath, `${data1}${data2}`, cb);
    });
  });
}

Сейчас мы проведём серию рефакторингов и получим в результате код, который является каноническим при работе с промисами. Итак, первая версия:

import fsp from 'fs/promises';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  // Промисы всегда должны возвращаться и строиться в цепочку!
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => {
      const promise = fsp.readFile(inputPath2, 'utf-8')
        .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
      return promise;
    });
  return result; // это промис
};

Хорошая новость — код стал понятнее и уменьшился в объёме. К тому же, из него целиком ушла обработка ошибок, так как промисы обрабатывают их автоматически и, если вызывающий код захочет их перехватывать, то сделает это самостоятельно через метод catch(). Но есть и плохая новость — код всё еще структурирован, как колбеки, "лесенкой". В этом коде не учитывается свойство промисов, связанное с возвратом из then().

import fsp from 'fs/promises';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => fsp.readFile(inputPath2, 'utf-8'))
    // then ниже берется от промиса readFile
    .then((data2) => fsp.writeFile(outputPath, `${как сюда может попасть data1?}${data2}`));
  return result;
};

Эта версия совсем плоская, именно к такому коду нужно стремиться в промисах. Но она таит в себе одну проблему. Если где-то в цепочке ниже нужны данные, которые были получены сверху, то придется протаскивать их сквозь всю цепочку. В примере выше это результат чтения первого файла. Переменная data1 недоступна в том месте, где происходит запись в файл. Основной выход из данной ситуации — создание переменных, через которые данные будут прокинуты дальше:

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}`));
};

Уже не так красиво, но всё еще плоско. Преимущество такого подхода становится всё более и более очевидным с увеличением количества промисов. Тем более далеко не всегда нужно передавать данные дальше.

Контроль асинхронных операций

Частая ошибка при работе с промисами – потеря контроля. Посмотрите на немного измененный код из примеров выше:

import fsp from 'fs/promises';

// Чего здесь не хватает?
const unionFiles = (inputPath1, inputPath2, outputPath) => {
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => {
      const promise = fsp.readFile(inputPath2, 'utf-8')
        .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
    });
  return result;
};

Этот код хоть и сработает во многих ситуациях, все же содержит серьезную ошибку. Промис внутри константы promise не возвращается наружу. Цепочка промисов прервалась. Любая ошибка в этом промисе пройдет незамеченной для внешнего кода. Нет гарантии что этот промис вообще успеет выполниться к тому моменту, когда этого будет ожидать вызывающий код.

Нарушение непрерывности (контроля) асинхронных операций частая ошибка даже у опытных программистов. В некоторых ситуациях ошибка заметна сразу, в других код начинает вести себя странно: часть запусков проходит без проблем, другая падает со странными ошибками.

Чтобы этого не происходило, нужно всегда убеждаться в непрерывности асинхронных операций. Завершение любой операции должно приводить к какой-то реакции, кто-то всегда должен ждать этого момента.

Динамическая цепочка

Иногда количество асинхронных операций заранее неизвестно, но они должны выполняться строго по очереди. Эту задачу можно решить, используя циклы или свертку. Какой бы способ не был выбран, сам принцип построения цепочки не поменяется. Цепочка промисов это всегда then().then().then()....

Единственный нюанс, который нужно учесть – начальный промис, с которого начнёт строиться цепочка. Если такого промиса нет, то его можно создать используя функцию Promise.resolve(). Она возвращает промис, который ничего не делает, но с него можно начинать свертку. Ниже пример, в котором последовательно читается набор файлов и возвращается массив их содержимого:

const filePaths = /* список путей до файлов */;

// Эта функция принимает на вход необязательное значение,
// которое появится в promise.then((<тут>) => ...)
// Начальное значение в данном случае – массив,
// в котором накапливаются данные из файлов
const initPromise = Promise.resolve([]);

// В then отдается функция, а не ее вызов!
const promise = filePaths.reduce((acc, path) => {
  // Аккумулятор – всегда промис, внутри которого массив с содержимым файлов
  const newAcc = acc.then((contents) =>
    // Читаем файл и добавляем его данные в аккумулятор
    fsp.readFile(path, 'utf-8').then((data) => contents.concat(data)));
  return newAcc;
}, initPromise);

// Если надо, продолжаем обработку
promise.then((contents) => /* обрабатываем все данные полученные из файлов */);

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

  1. Для чего в промисах используются return?

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

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

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

Для полного доступа к курсу нужен базовый план

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
Иконка программы Фронтенд-разработчик
Профессия
с нуля
Разработка фронтенд-компонентов для веб-приложений
30 марта 10 месяцев
Иконка программы Онлайн-буткемп. Фронтенд-разработчик
Профессия
Новый с нуля
Интенсивное обучение профессии в режиме полного дня
20 апреля 4 месяца
Иконка программы Node.js-разработчик
Профессия
с нуля
Разработка бэкенд-компонентов для веб-приложений
30 марта 10 месяцев
Иконка программы Fullstack-разработчик
Профессия
с нуля
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
30 марта 16 месяцев
Иконка программы Инженер по автоматизированному тестированию на JavaScript
Профессия
Новый В разработке с нуля
Автоматизированное тестирование веб-приложений на JavaScript
дата определяется 10 месяцев

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

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

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»