Promisify, callbackify, asyncify
Адаптеры между тремя контрактами асинхронных функций в JavaScript: sync, callback-last (error-first) и Promise-based. Позволяют связать разные стили в одной кодовой базе.
Что это / Зачем
В Node.js и JS исторически три контракта: синхронные функции, callback-last (error-first) и Promise/async. Адаптеры превращают один в другой:
- promisify — callback API → Promise (
util.promisifyвстроен в Node, есть с самого начала) - callbackify — Promise/async → callback API (обратная совместимость с legacy)
- asyncify — синхронная функция → асинхронная (с callback)
API / Синтаксис
// promisify — ручная реализация
const promisify = (fn) => (...args) =>
new Promise((resolve, reject) =>
fn(...args, (err, res) => (err ? reject(err) : resolve(res))));
const readFile = promisify(fs.readFile);
const data = await readFile('file.txt', 'utf8');
// callbackify — обратная сторона
const callbackify = (asyncFn) => (...args) => {
const callback = args.pop();
asyncFn(...args).then(
(res) => callback(null, res),
(err) => callback(err),
);
};
// asyncify — sync функция в callback-стиль
const asyncify = (fn) => (...args) => {
const callback = args.pop();
setTimeout(() => {
try { callback(null, fn(...args)); }
catch (err) { callback(err); }
}, 0);
};
// Встроено в Node
const { promisify } = require('node:util');
Ключевые моменты
- Error-first callback:
callback(null, result)успех,callback(err)ошибка. - Паттерн "две стрелочные функции" (
(fn) => (...args) => ...) V8 распознаёт и хорошо оптимизирует — внешняя функция фактически нужна для сохранения аргумента в замыкании. - Длинное каррирование (3-4 функции в цепочке) V8 оптимизирует хуже.
util.promisifyв Node имеет более сложную реализацию, чем учебная.- Адаптер может выполнять обёрнутую функцию синхронно ИЛИ асинхронно — это решение автора адаптера.
Подводные камни
args.pop()мутирует массив — приводит к перевыделению памяти, ломает оптимизацию. Лучшеargs.at(-1)+args.slice(0, -1).arguments(старый объект) ломает оптимизацию всей функции — после появления rest-оператора (...args) использоватьargumentsнельзя.- setTimeout — не часть языка, а host API (в браузере и Node реализованы по-разному). В Node под капотом enroll/unenroll, реальных таймеров ОС на порядок меньше.
- try/catch с 2014+ оптимизируется отлично (после перехода V8 с Crankshaft на Ignition+TurboFan). Можно "обвешаться try/catch".
🎓 Источники
- ⚡ [Асинхронное программирование и оптимизация для V8 asyncify, promisify, callbackify] · 2025-12-20 · YouTube
- Тезисы:
- V8 спец-оптимизирует паттерн из двух стрелочных функций — кэширует вложенную функцию вместе с шаблоном контекста.
- Самый быстрый JS близок к C — меньше динамики, лучше оптимизация.
- args.pop() — главная проблема в типичной реализации adapter'а.
- setTimeout — host API, в Node стоит дешевле благодаря enroll/unenroll.
- try/catch/finally больше не вызывают деоптимизации.
- На байт-коде try/catch — это набор if-ов (jump'ов CPU).
- Цитата:
«Наиболее эффективный, наиболее быстрый код в JavaScript получается тот, который ближе всего к коду, написанному на языке C. Чем больше мы избавляемся от динамичности, тем проще V8 оптимизировать этот код.»
- Цитата:
«setTimeout — это внешний API, которая реализована совершенно по-разному в браузере и в ноде. То есть в JavaScript вообще setTimeout нет.»
- Тезисы:
- 🎓 [Асинхронные адаптеры promisify, callbackify, asyncify...] · 2018-12-18 · YouTube
- 🎓 [Архив 2018 - Часть 15 адаптеры callbackify, promisify, асинхронная очередь] · 2020-01-13 · YouTube
- 🎓 [Асинхронное программирование callbackify, class adapter, async iterator] · 2025-12-23 · YouTube