W3docs

Промисификация в JavaScript

Промисификация в JavaScript: оборачиваем колбэки в Promise, создаём promisify(), обрабатываем многоаргументные колбэки. Примеры с запуском.

Что такое промисификация?

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

На этой странице рассматривается, как обернуть один error-first колбэк API, как создать многоразовый универсальный хелпер promisify(), как обрабатывать колбэки, возвращающие несколько результатов, и в каких случаях промисификация не работает.

Зачем промисифицировать?

Старые JavaScript API и большинство стандартной библиотеки Node.js сообщают о результатах через переданный вами колбэк. Такой стиль быстро порождает вложенность и разбрасывает обработку ошибок:

getUser(id, (err, user) => {
  if (err) return handleError(err);
  getOrders(user, (err, orders) => {
    if (err) return handleError(err);
    getTotal(orders, (err, total) => {
      if (err) return handleError(err);
      console.log(total);
    });
  });
});

Если те же функции возвращали бы промисы, логика превращается в единую линейную цепочку (или несколько строк с await) с одним .catch() для всего потока. Промисификация — это мост между двумя этими мирами; подробнее о колбэках см. в Колбэки и что дальше.

Соглашение error-first колбэков

Прежде чем что-либо оборачивать, нужно знать форму того, что вы оборачиваете. Колбэки Node.js следуют соглашению error-first (или «Node-style»): колбэк является последним аргументом и вызывается как callback(error, result).

  • При ошибке error — это объект Error, а result равен undefined.
  • При успехе error равен null, а result содержит значение.

Промисификация отображает это напрямую: ненулевой error превращается в reject(error), а успешный result — в resolve(result).

Оборачивание одного колбэк API

Вот основной паттерн. Мы оборачиваем функцию в стиле колбэков в новый Promise, вызывая reject для ошибки и resolve для значения. Пример имитирует error-first API с помощью setTimeout, поэтому работает где угодно, в том числе в браузере:


javascript— editable

Обёртка принимает тот же аргумент id, передаёт его дальше и подставляет собственный колбэк, который соединяет вызов с resolve/reject. Вызывающему коду колбэк больше не нужен.

Использование обёртки с async/await

Настоящее преимущество функции, возвращающей промис, состоит в том, что она работает с async/await, превращая асинхронный код в нечто, что читается сверху вниз:


javascript— editable

Универсальный хелпер promisify()

Писать обёртку вручную для каждой функции утомительно. Универсальный хелпер принимает любую error-first функцию и возвращает её версию, возвращающую промис. Хитрость в том, чтобы собрать все исходные аргументы с помощью rest-параметра и затем добавить собственный колбэк:


javascript— editable

Поскольку хелпер использует ...args и передаёт this, он работает для функций с любым количеством ведущих аргументов. В Node.js стандартная библиотека поставляет точно такой же механизм в виде util.promisify, поэтому писать собственный там редко нужно:

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

readFile('file.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));

Обработка многоаргументных колбэков

Простой хелпер предполагает, что колбэк возвращает один результат: callback(err, result). Некоторые API передают несколько значений, например callback(err, header, body). При обычном resolve(result) всё после первого значения будет молча потеряно.

Промис может разрешиться только одним значением, поэтому соберите дополнительные аргументы в array (или object) и разрешите промис с ним:


javascript— editable

Node.js util.promisify поддерживает ту же идею через специальный символ (util.promisify.custom), но для разовых функций array — самый простой подход.

Ограничения и подводные камни

Промисификация механична, но имеет реальные границы:

  • Ожидается соглашение error-first. Если функция сигнализирует об ошибках иначе — например, через boolean возвращаемое значение, выброшенное исключение или порядок (result, err) — универсальный хелпер неправильно её интерпретирует. Такие функции оборачивайте вручную.
  • Обрабатывается только одно завершение. Промисы устанавливаются один раз. Функция, вызывающая свой колбэк многократно (события, потоки, setInterval, колбэк прогресса), не может быть промисифицирована — только первый вызов разрешит промис, последующие будут проигнорированы. Для повторяющихся значений используйте событийный API или async-итератор.
  • Промис нельзя отменить. Если исходный колбэк API поддерживает отмену (например, сброс таймера), эта возможность теряется при скрытии за промисом.
  • Обёртка меняет сигнатуру вызова. Вызывающий код теперь должен использовать .then/await вместо передачи колбэка. Не промисифицируйте функцию, которую другой код всё ещё вызывает в стиле колбэков, не сохранив обе версии.
  • Throw внутри executor всё равно вызывает reject. Код, который выполняется синхронно внутри new Promise((resolve, reject) => { ... }), перехватывается и превращается в отклонение — но ошибка, выброшенная позднее внутри асинхронного колбэка, не перехватывается автоматически, именно поэтому необходимо явно вызывать reject(err).

Лучшие практики

  • Промисифицируйте на границе. Конвертируйте I/O и таймерные API один раз, рядом с местом их входа в ваш код, и оставляйте остальную кодовую базу основанной на промисах.
  • Отдавайте предпочтение встроенным средствам. В Node.js используйте util.promisify (или модули fs/promises, dns/promises и т.д.) прежде чем писать обёртку вручную.
  • Всегда обрабатывайте отклонение. Добавляйте .catch() или оборачивайте await в try/catch; необработанное отклонение может завершить процесс Node.js.
  • Давайте предсказуемые имена. Распространённое соглашение — добавлять суффикс Async к версии с промисом (readFileAsync), чтобы оба стиля могли сосуществовать.

Связанные темы

  • JavaScript Promise — object, который вы создаёте при промисификации.
  • Цепочки промисов — последовательно вызывайте промисифицированные функции.
  • Async/Await — синтаксис, который делает промисифицированные функции похожими на синхронный код.
  • Колбэки и что дальше — паттерн, из которого вы конвертируете.
  • Promise API — объединяйте несколько промисифицированных вызовов с помощью Promise.all и других методов.

Практика

Практика
Какова основная функция промисификации в JavaScript?
Какова основная функция промисификации в JavaScript?
Was this page helpful?