Промисификация в 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, поэтому работает где угодно, в том числе в браузере:
Обёртка принимает тот же аргумент id, передаёт его дальше и подставляет собственный колбэк, который соединяет вызов с resolve/reject. Вызывающему коду колбэк больше не нужен.
Использование обёртки с async/await
Настоящее преимущество функции, возвращающей промис, состоит в том, что она работает с async/await, превращая асинхронный код в нечто, что читается сверху вниз:
Универсальный хелпер promisify()
Писать обёртку вручную для каждой функции утомительно. Универсальный хелпер принимает любую error-first функцию и возвращает её версию, возвращающую промис. Хитрость в том, чтобы собрать все исходные аргументы с помощью rest-параметра и затем добавить собственный колбэк:
Поскольку хелпер использует ...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) и разрешите промис с ним:
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и других методов.